Add project files.

This commit is contained in:
Zura
2025-08-21 21:38:57 +02:00
parent 54ede2292c
commit dd2e9bf7d1
193 changed files with 32957 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
using MareSynchronos.API.Data.Enum;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.PlayerData.Factories;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.CharaData.Models;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
{
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly DalamudUtilService _dalamudUtilService;
private readonly IpcManager _ipcManager;
private readonly HashSet<HandledCharaDataEntry> _handledCharaData = [];
public IEnumerable<HandledCharaDataEntry> HandledCharaData => _handledCharaData;
public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator,
GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService,
IpcManager ipcManager)
: base(logger, mediator)
{
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_dalamudUtilService = dalamudUtilService;
_ipcManager = ipcManager;
mediator.Subscribe<GposeEndMessage>(this, (_) =>
{
foreach (var chara in _handledCharaData)
{
RevertHandledChara(chara);
}
});
mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (_) => HandleCutsceneFrameworkUpdate());
}
private void HandleCutsceneFrameworkUpdate()
{
if (!_dalamudUtilService.IsInGpose) return;
foreach (var entry in _handledCharaData.ToList())
{
var chara = _dalamudUtilService.GetGposeCharacterFromObjectTableByName(entry.Name, onlyGposeCharacters: true);
if (chara is null)
{
RevertChara(entry.Name, entry.CustomizePlus).GetAwaiter().GetResult();
_handledCharaData.Remove(entry);
}
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
foreach (var chara in _handledCharaData)
{
RevertHandledChara(chara);
}
}
public async Task RevertChara(string name, Guid? cPlusId)
{
Guid applicationId = Guid.NewGuid();
await _ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).ConfigureAwait(false);
if (cPlusId != null)
{
await _ipcManager.CustomizePlus.RevertByIdAsync(cPlusId).ConfigureAwait(false);
}
using var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
() => _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, _dalamudUtilService.IsInGpose)?.Address ?? IntPtr.Zero, false)
.ConfigureAwait(false);
if (handler.Address != nint.Zero)
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, CancellationToken.None).ConfigureAwait(false);
}
public async Task<bool> RevertHandledChara(string name)
{
var handled = _handledCharaData.FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.Ordinal));
if (handled == null) return false;
_handledCharaData.Remove(handled);
await _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(handled.Name, handled.CustomizePlus)).ConfigureAwait(false);
return true;
}
public Task RevertHandledChara(HandledCharaDataEntry? handled)
{
if (handled == null) return Task.CompletedTask;
_handledCharaData.Remove(handled);
return _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(handled.Name, handled.CustomizePlus));
}
internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry)
{
_handledCharaData.Add(handledCharaDataEntry);
}
public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
{
foreach (var handledData in _handledCharaData)
{
if (newData.TryGetValue(handledData.MetaInfo.FullId, out var metaInfo) && metaInfo != null)
{
handledData.MetaInfo = metaInfo;
}
}
}
public async Task<GameObjectHandler?> TryCreateGameObjectHandler(string name, bool gPoseOnly = false)
{
var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
() => _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, gPoseOnly && _dalamudUtilService.IsInGpose)?.Address ?? IntPtr.Zero, false)
.ConfigureAwait(false);
if (handler.Address == nint.Zero) return null;
return handler;
}
public async Task<GameObjectHandler?> TryCreateGameObjectHandler(int index)
{
var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
() => _dalamudUtilService.GetCharacterFromObjectTableByIndex(index)?.Address ?? IntPtr.Zero, false)
.ConfigureAwait(false);
if (handler.Address == nint.Zero) return null;
return handler;
}
}

View File

@@ -0,0 +1,303 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using K4os.Compression.LZ4.Legacy;
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.FileCache;
using MareSynchronos.PlayerData.Factories;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.CharaData;
using MareSynchronos.Services.CharaData.Models;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI.Files;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public sealed class CharaDataFileHandler : IDisposable
{
private readonly DalamudUtilService _dalamudUtilService;
private readonly FileCacheManager _fileCacheManager;
private readonly FileDownloadManager _fileDownloadManager;
private readonly FileUploadManager _fileUploadManager;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly ILogger<CharaDataFileHandler> _logger;
private readonly MareCharaFileDataFactory _mareCharaFileDataFactory;
private readonly PlayerDataFactory _playerDataFactory;
private int _globalFileCounter = 0;
public CharaDataFileHandler(ILogger<CharaDataFileHandler> logger, FileDownloadManagerFactory fileDownloadManagerFactory, FileUploadManager fileUploadManager, FileCacheManager fileCacheManager,
DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, PlayerDataFactory playerDataFactory)
{
_fileDownloadManager = fileDownloadManagerFactory.Create();
_logger = logger;
_fileUploadManager = fileUploadManager;
_fileCacheManager = fileCacheManager;
_dalamudUtilService = dalamudUtilService;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_playerDataFactory = playerDataFactory;
_mareCharaFileDataFactory = new(fileCacheManager);
}
public void ComputeMissingFiles(CharaDataDownloadDto charaDataDownloadDto, out Dictionary<string, string> modPaths, out List<FileReplacementData> missingFiles)
{
modPaths = [];
missingFiles = [];
foreach (var file in charaDataDownloadDto.FileGamePaths)
{
var localCacheFile = _fileCacheManager.GetFileCacheByHash(file.HashOrFileSwap);
if (localCacheFile == null)
{
var existingFile = missingFiles.Find(f => string.Equals(f.Hash, file.HashOrFileSwap, StringComparison.Ordinal));
if (existingFile == null)
{
missingFiles.Add(new FileReplacementData()
{
Hash = file.HashOrFileSwap,
GamePaths = [file.GamePath]
});
}
else
{
existingFile.GamePaths = existingFile.GamePaths.Concat([file.GamePath]).ToArray();
}
}
else
{
modPaths[file.GamePath] = localCacheFile.ResolvedFilepath;
}
}
foreach (var swap in charaDataDownloadDto.FileSwaps)
{
modPaths[swap.GamePath] = swap.HashOrFileSwap;
}
}
public async Task<CharacterData?> CreatePlayerData()
{
var chara = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
if (_dalamudUtilService.IsInGpose)
{
chara = (IPlayerCharacter?)(await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(chara.Name.TextValue, _dalamudUtilService.IsInGpose).ConfigureAwait(false));
}
if (chara == null)
return null;
using var tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
() => _dalamudUtilService.GetCharacterFromObjectTableByIndex(chara.ObjectIndex)?.Address ?? IntPtr.Zero, isWatched: false).ConfigureAwait(false);
PlayerData.Data.CharacterData newCdata = new();
var fragment = await _playerDataFactory.BuildCharacterData(tempHandler, CancellationToken.None).ConfigureAwait(false);
newCdata.SetFragment(ObjectKind.Player, fragment);
if (newCdata.FileReplacements.TryGetValue(ObjectKind.Player, out var playerData) && playerData != null)
{
foreach (var data in playerData.Select(g => g.GamePaths))
{
data.RemoveWhere(g => g.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)
|| g.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase)
|| g.EndsWith(".scd", StringComparison.OrdinalIgnoreCase)
|| (g.EndsWith(".avfx", StringComparison.OrdinalIgnoreCase)
&& !g.Contains("/weapon/", StringComparison.OrdinalIgnoreCase)
&& !g.Contains("/equipment/", StringComparison.OrdinalIgnoreCase))
|| (g.EndsWith(".atex", StringComparison.OrdinalIgnoreCase)
&& !g.Contains("/weapon/", StringComparison.OrdinalIgnoreCase)
&& !g.Contains("/equipment/", StringComparison.OrdinalIgnoreCase)));
}
playerData.RemoveWhere(g => g.GamePaths.Count == 0);
}
return newCdata.ToAPI();
}
public void Dispose()
{
_fileDownloadManager.Dispose();
}
public async Task DownloadFilesAsync(GameObjectHandler tempHandler, List<FileReplacementData> missingFiles, Dictionary<string, string> modPaths, CancellationToken token)
{
await _fileDownloadManager.InitiateDownloadList(tempHandler, missingFiles, token).ConfigureAwait(false);
await _fileDownloadManager.DownloadFiles(tempHandler, missingFiles, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
foreach (var file in missingFiles.SelectMany(m => m.GamePaths, (FileEntry, GamePath) => (FileEntry.Hash, GamePath)))
{
var localFile = _fileCacheManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath;
if (localFile == null)
{
throw new FileNotFoundException("File not found locally.");
}
modPaths[file.GamePath] = localFile;
}
}
public Task<(MareCharaFileHeader loadedCharaFile, long expectedLength)> LoadCharaFileHeader(string filePath)
{
try
{
using var unwrapped = File.OpenRead(filePath);
using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression);
using var reader = new BinaryReader(lz4Stream);
var loadedCharaFile = MareCharaFileHeader.FromBinaryReader(filePath, reader);
_logger.LogInformation("Read Mare Chara File");
_logger.LogInformation("Version: {ver}", (loadedCharaFile?.Version ?? -1));
long expectedLength = 0;
if (loadedCharaFile != null)
{
_logger.LogTrace("Data");
foreach (var item in loadedCharaFile.CharaFileData.FileSwaps)
{
foreach (var gamePath in item.GamePaths)
{
_logger.LogTrace("Swap: {gamePath} => {fileSwapPath}", gamePath, item.FileSwapPath);
}
}
var itemNr = 0;
foreach (var item in loadedCharaFile.CharaFileData.Files)
{
itemNr++;
expectedLength += item.Length;
foreach (var gamePath in item.GamePaths)
{
_logger.LogTrace("File {itemNr}: {gamePath} = {len}", itemNr, gamePath, item.Length.ToByteString());
}
}
_logger.LogInformation("Expected length: {expected}", expectedLength.ToByteString());
}
else
{
throw new InvalidOperationException("MCDF Header was null");
}
return Task.FromResult((loadedCharaFile, expectedLength));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not parse MCDF header of file {file}", filePath);
throw;
}
}
public Dictionary<string, string> McdfExtractFiles(MareCharaFileHeader? charaFileHeader, long expectedLength, List<string> extractedFiles)
{
if (charaFileHeader == null) return [];
using var lz4Stream = new LZ4Stream(File.OpenRead(charaFileHeader.FilePath), LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression);
using var reader = new BinaryReader(lz4Stream);
MareCharaFileHeader.AdvanceReaderToData(reader);
long totalRead = 0;
Dictionary<string, string> gamePathToFilePath = new(StringComparer.Ordinal);
foreach (var fileData in charaFileHeader.CharaFileData.Files)
{
var fileName = Path.Combine(_fileCacheManager.CacheFolder, "mare_" + _globalFileCounter++ + ".tmp");
extractedFiles.Add(fileName);
var length = fileData.Length;
var bufferSize = length;
using var fs = File.OpenWrite(fileName);
using var wr = new BinaryWriter(fs);
_logger.LogTrace("Reading {length} of {fileName}", length.ToByteString(), fileName);
var buffer = reader.ReadBytes(bufferSize);
wr.Write(buffer);
wr.Flush();
wr.Close();
if (buffer.Length == 0) throw new EndOfStreamException("Unexpected EOF");
foreach (var path in fileData.GamePaths)
{
gamePathToFilePath[path] = fileName;
_logger.LogTrace("{path} => {fileName} [{hash}]", path, fileName, fileData.Hash);
}
totalRead += length;
_logger.LogTrace("Read {read}/{expected} bytes", totalRead.ToByteString(), expectedLength.ToByteString());
}
return gamePathToFilePath;
}
public async Task UpdateCharaDataAsync(CharaDataExtendedUpdateDto updateDto)
{
var data = await CreatePlayerData().ConfigureAwait(false);
if (data != null)
{
var hasGlamourerData = data.GlamourerData.TryGetValue(ObjectKind.Player, out var playerDataString);
if (!hasGlamourerData) updateDto.GlamourerData = null;
else updateDto.GlamourerData = playerDataString;
var hasCustomizeData = data.CustomizePlusData.TryGetValue(ObjectKind.Player, out var customizeDataString);
if (!hasCustomizeData) updateDto.CustomizeData = null;
else updateDto.CustomizeData = customizeDataString;
updateDto.ManipulationData = data.ManipulationData;
var hasFiles = data.FileReplacements.TryGetValue(ObjectKind.Player, out var fileReplacements);
if (!hasFiles)
{
updateDto.FileGamePaths = [];
updateDto.FileSwaps = [];
}
else
{
updateDto.FileGamePaths = [.. fileReplacements!.Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))];
updateDto.FileSwaps = [.. fileReplacements!.Where(u => !string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.FileSwapPath, path))];
}
}
}
internal async Task SaveCharaFileAsync(string description, string filePath)
{
var tempFilePath = filePath + ".tmp";
try
{
var data = await CreatePlayerData().ConfigureAwait(false);
if (data == null) return;
var mareCharaFileData = _mareCharaFileDataFactory.Create(description, data);
MareCharaFileHeader output = new(MareCharaFileHeader.CurrentVersion, mareCharaFileData);
using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
using var lz4 = new LZ4Stream(fs, LZ4StreamMode.Compress, LZ4StreamFlags.HighCompression);
using var writer = new BinaryWriter(lz4);
output.WriteToStream(writer);
foreach (var item in output.CharaFileData.Files)
{
var file = _fileCacheManager.GetFileCacheByHash(item.Hash)!;
_logger.LogDebug("Saving to MCDF: {hash}:{file}", item.Hash, file.ResolvedFilepath);
_logger.LogDebug("\tAssociated GamePaths:");
foreach (var path in item.GamePaths)
{
_logger.LogDebug("\t{path}", path);
}
var fsRead = File.OpenRead(file.ResolvedFilepath);
await using (fsRead.ConfigureAwait(false))
{
using var br = new BinaryReader(fsRead);
byte[] buffer = new byte[item.Length];
br.Read(buffer, 0, item.Length);
writer.Write(buffer);
}
}
writer.Flush();
await lz4.FlushAsync().ConfigureAwait(false);
await fs.FlushAsync().ConfigureAwait(false);
fs.Close();
File.Move(tempFilePath, filePath, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failure Saving Mare Chara File, deleting output");
File.Delete(tempFilePath);
}
}
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
{
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,696 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.Interop;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.Services.CharaData.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Numerics;
using System.Text.Json.Nodes;
namespace MareSynchronos.Services.CharaData;
public class CharaDataGposeTogetherManager : DisposableMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly IpcCallerBrio _brio;
private readonly SemaphoreSlim _charaDataCreationSemaphore = new(1, 1);
private readonly CharaDataFileHandler _charaDataFileHandler;
private readonly CharaDataManager _charaDataManager;
private readonly DalamudUtilService _dalamudUtil;
private readonly Dictionary<string, GposeLobbyUserData> _usersInLobby = [];
private readonly VfxSpawnManager _vfxSpawnManager;
private (CharacterData ApiData, CharaDataDownloadDto Dto)? _lastCreatedCharaData;
private PoseData? _lastDeltaPoseData;
private PoseData? _lastFullPoseData;
private WorldData? _lastWorldData;
private CancellationTokenSource _lobbyCts = new();
private int _poseGenerationExecutions = 0;
public CharaDataGposeTogetherManager(ILogger<CharaDataGposeTogetherManager> logger, MareMediator mediator,
ApiController apiController, IpcCallerBrio brio, DalamudUtilService dalamudUtil, VfxSpawnManager vfxSpawnManager,
CharaDataFileHandler charaDataFileHandler, CharaDataManager charaDataManager) : base(logger, mediator)
{
Mediator.Subscribe<GposeLobbyUserJoin>(this, (msg) =>
{
OnUserJoinLobby(msg.UserData);
});
Mediator.Subscribe<GPoseLobbyUserLeave>(this, (msg) =>
{
OnUserLeaveLobby(msg.UserData);
});
Mediator.Subscribe<GPoseLobbyReceiveCharaData>(this, (msg) =>
{
OnReceiveCharaData(msg.CharaDataDownloadDto);
});
Mediator.Subscribe<GPoseLobbyReceivePoseData>(this, (msg) =>
{
OnReceivePoseData(msg.UserData, msg.PoseData);
});
Mediator.Subscribe<GPoseLobbyReceiveWorldData>(this, (msg) =>
{
OnReceiveWorldData(msg.UserData, msg.WorldData);
});
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
{
if (_usersInLobby.Count > 0 && !string.IsNullOrEmpty(CurrentGPoseLobbyId))
{
JoinGPoseLobby(CurrentGPoseLobbyId, isReconnecting: true);
}
else
{
LeaveGPoseLobby();
}
});
Mediator.Subscribe<GposeStartMessage>(this, (msg) =>
{
OnEnterGpose();
});
Mediator.Subscribe<GposeEndMessage>(this, (msg) =>
{
OnExitGpose();
});
Mediator.Subscribe<FrameworkUpdateMessage>(this, (msg) =>
{
OnFrameworkUpdate();
});
Mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (msg) =>
{
OnCutsceneFrameworkUpdate();
});
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
LeaveGPoseLobby();
});
_apiController = apiController;
_brio = brio;
_dalamudUtil = dalamudUtil;
_vfxSpawnManager = vfxSpawnManager;
_charaDataFileHandler = charaDataFileHandler;
_charaDataManager = charaDataManager;
}
public string? CurrentGPoseLobbyId { get; private set; }
public string? LastGPoseLobbyId { get; private set; }
public IEnumerable<GposeLobbyUserData> UsersInLobby => _usersInLobby.Values;
public (bool SameMap, bool SameServer, bool SameEverything) IsOnSameMapAndServer(GposeLobbyUserData data)
{
return (data.Map.RowId == _lastWorldData?.LocationInfo.MapId, data.WorldData?.LocationInfo.ServerId == _lastWorldData?.LocationInfo.ServerId, data.WorldData?.LocationInfo == _lastWorldData?.LocationInfo);
}
public async Task PushCharacterDownloadDto()
{
var playerData = await _charaDataFileHandler.CreatePlayerData().ConfigureAwait(false);
if (playerData == null) return;
if (!string.Equals(playerData.DataHash.Value, _lastCreatedCharaData?.ApiData.DataHash.Value, StringComparison.Ordinal))
{
List<GamePathEntry> filegamePaths = [.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player]
.Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))];
List<GamePathEntry> fileSwapPaths = [.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player]
.Where(u => !string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.FileSwapPath, path))];
await _charaDataManager.UploadFiles([.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player]
.Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))])
.ConfigureAwait(false);
CharaDataDownloadDto charaDataDownloadDto = new($"GPOSELOBBY:{CurrentGPoseLobbyId}", new(_apiController.UID))
{
UpdatedDate = DateTime.UtcNow,
ManipulationData = playerData.ManipulationData,
CustomizeData = playerData.CustomizePlusData[API.Data.Enum.ObjectKind.Player],
FileGamePaths = filegamePaths,
FileSwaps = fileSwapPaths,
GlamourerData = playerData.GlamourerData[API.Data.Enum.ObjectKind.Player],
};
_lastCreatedCharaData = (playerData, charaDataDownloadDto);
}
ForceResendOwnData();
if (_lastCreatedCharaData != null)
await _apiController.GposeLobbyPushCharacterData(_lastCreatedCharaData.Value.Dto)
.ConfigureAwait(false);
}
internal void CreateNewLobby()
{
_ = Task.Run(async () =>
{
ClearLobby();
CurrentGPoseLobbyId = await _apiController.GposeLobbyCreate().ConfigureAwait(false);
if (!string.IsNullOrEmpty(CurrentGPoseLobbyId))
{
_ = GposeWorldPositionBackgroundTask(_lobbyCts.Token);
_ = GposePoseDataBackgroundTask(_lobbyCts.Token);
}
});
}
internal void JoinGPoseLobby(string joinLobbyId, bool isReconnecting = false)
{
_ = Task.Run(async () =>
{
var otherUsers = await _apiController.GposeLobbyJoin(joinLobbyId).ConfigureAwait(false);
ClearLobby();
if (otherUsers.Any())
{
LastGPoseLobbyId = string.Empty;
foreach (var user in otherUsers)
{
OnUserJoinLobby(user);
}
CurrentGPoseLobbyId = joinLobbyId;
_ = GposeWorldPositionBackgroundTask(_lobbyCts.Token);
_ = GposePoseDataBackgroundTask(_lobbyCts.Token);
}
else
{
LeaveGPoseLobby();
LastGPoseLobbyId = string.Empty;
}
});
}
internal void LeaveGPoseLobby()
{
_ = Task.Run(async () =>
{
var left = await _apiController.GposeLobbyLeave().ConfigureAwait(false);
if (left)
{
if (_usersInLobby.Count != 0)
{
LastGPoseLobbyId = CurrentGPoseLobbyId;
}
ClearLobby(revertCharas: true);
}
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
ClearLobby(revertCharas: true);
}
}
private void ClearLobby(bool revertCharas = false)
{
_lobbyCts.Cancel();
_lobbyCts.Dispose();
_lobbyCts = new();
CurrentGPoseLobbyId = string.Empty;
foreach (var user in _usersInLobby.ToDictionary())
{
if (revertCharas)
_charaDataManager.RevertChara(user.Value.HandledChara);
OnUserLeaveLobby(user.Value.UserData);
}
_usersInLobby.Clear();
}
private string CreateJsonFromPoseData(PoseData? poseData)
{
if (poseData == null) return "{}";
var node = new JsonObject();
node["Bones"] = new JsonObject();
foreach (var bone in poseData.Value.Bones)
{
node["Bones"]![bone.Key] = new JsonObject();
node["Bones"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}";
node["Bones"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}";
node["Bones"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}";
}
node["MainHand"] = new JsonObject();
foreach (var bone in poseData.Value.MainHand)
{
node["MainHand"]![bone.Key] = new JsonObject();
node["MainHand"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}";
node["MainHand"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}";
node["MainHand"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}";
}
node["OffHand"] = new JsonObject();
foreach (var bone in poseData.Value.OffHand)
{
node["OffHand"]![bone.Key] = new JsonObject();
node["OffHand"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}";
node["OffHand"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}";
node["OffHand"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}";
}
return node.ToJsonString();
}
private PoseData CreatePoseDataFromJson(string json, PoseData? fullPoseData = null)
{
PoseData output = new();
output.Bones = new(StringComparer.Ordinal);
output.MainHand = new(StringComparer.Ordinal);
output.OffHand = new(StringComparer.Ordinal);
float getRounded(string number)
{
return float.Round(float.Parse(number, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture), 5);
}
BoneData createBoneData(JsonNode boneJson)
{
BoneData outputBoneData = new();
outputBoneData.Exists = true;
var posString = boneJson["Position"]!.ToString();
var pos = posString.Split(",", StringSplitOptions.TrimEntries);
outputBoneData.PositionX = getRounded(pos[0]);
outputBoneData.PositionY = getRounded(pos[1]);
outputBoneData.PositionZ = getRounded(pos[2]);
var scaString = boneJson["Scale"]!.ToString();
var sca = scaString.Split(",", StringSplitOptions.TrimEntries);
outputBoneData.ScaleX = getRounded(sca[0]);
outputBoneData.ScaleY = getRounded(sca[1]);
outputBoneData.ScaleZ = getRounded(sca[2]);
var rotString = boneJson["Rotation"]!.ToString();
var rot = rotString.Split(",", StringSplitOptions.TrimEntries);
outputBoneData.RotationX = getRounded(rot[0]);
outputBoneData.RotationY = getRounded(rot[1]);
outputBoneData.RotationZ = getRounded(rot[2]);
outputBoneData.RotationW = getRounded(rot[3]);
return outputBoneData;
}
var node = JsonNode.Parse(json)!;
var bones = node["Bones"]!.AsObject();
foreach (var bone in bones)
{
string name = bone.Key;
var boneJson = bone.Value!.AsObject();
BoneData outputBoneData = createBoneData(boneJson);
if (fullPoseData != null)
{
if (fullPoseData.Value.Bones.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData)
{
output.Bones[name] = outputBoneData;
}
}
else
{
output.Bones[name] = outputBoneData;
}
}
var mainHand = node["MainHand"]!.AsObject();
foreach (var bone in mainHand)
{
string name = bone.Key;
var boneJson = bone.Value!.AsObject();
BoneData outputBoneData = createBoneData(boneJson);
if (fullPoseData != null)
{
if (fullPoseData.Value.MainHand.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData)
{
output.MainHand[name] = outputBoneData;
}
}
else
{
output.MainHand[name] = outputBoneData;
}
}
var offhand = node["OffHand"]!.AsObject();
foreach (var bone in offhand)
{
string name = bone.Key;
var boneJson = bone.Value!.AsObject();
BoneData outputBoneData = createBoneData(boneJson);
if (fullPoseData != null)
{
if (fullPoseData.Value.OffHand.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData)
{
output.OffHand[name] = outputBoneData;
}
}
else
{
output.OffHand[name] = outputBoneData;
}
}
if (fullPoseData != null)
output.IsDelta = true;
return output;
}
private async Task GposePoseDataBackgroundTask(CancellationToken ct)
{
_lastFullPoseData = null;
_lastDeltaPoseData = null;
_poseGenerationExecutions = 0;
while (!ct.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(10), ct).ConfigureAwait(false);
if (!_dalamudUtil.IsInGpose) continue;
if (_usersInLobby.Count == 0) continue;
try
{
var chara = await _dalamudUtil.GetPlayerCharacterAsync().ConfigureAwait(false);
if (_dalamudUtil.IsInGpose)
{
chara = (IPlayerCharacter?)(await _dalamudUtil.GetGposeCharacterFromObjectTableByNameAsync(chara.Name.TextValue, _dalamudUtil.IsInGpose).ConfigureAwait(false));
}
if (chara == null || chara.Address == nint.Zero) continue;
var poseJson = await _brio.GetPoseAsync(chara.Address).ConfigureAwait(false);
if (string.IsNullOrEmpty(poseJson)) continue;
var lastFullData = _poseGenerationExecutions++ >= 12 ? null : _lastFullPoseData;
lastFullData = _forceResendFullPose ? _lastFullPoseData : lastFullData;
var poseData = CreatePoseDataFromJson(poseJson, lastFullData);
if (!poseData.IsDelta)
{
_lastFullPoseData = poseData;
_lastDeltaPoseData = null;
_poseGenerationExecutions = 0;
}
bool deltaIsSame = _lastDeltaPoseData != null &&
(poseData.Bones.Keys.All(k => _lastDeltaPoseData.Value.Bones.ContainsKey(k)
&& poseData.Bones.Values.All(k => _lastDeltaPoseData.Value.Bones.ContainsValue(k))));
if (_forceResendFullPose || ((poseData.Bones.Any() || poseData.MainHand.Any() || poseData.OffHand.Any())
&& (!poseData.IsDelta || (poseData.IsDelta && !deltaIsSame))))
{
_forceResendFullPose = false;
await _apiController.GposeLobbyPushPoseData(poseData).ConfigureAwait(false);
}
if (poseData.IsDelta)
_lastDeltaPoseData = poseData;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during Pose Data Generation");
}
}
}
private async Task GposeWorldPositionBackgroundTask(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(_dalamudUtil.IsInGpose ? 10 : 1), ct).ConfigureAwait(false);
// if there are no players in lobby, don't do anything
if (_usersInLobby.Count == 0) continue;
try
{
// get own player data
var player = (Dalamud.Game.ClientState.Objects.Types.ICharacter?)(await _dalamudUtil.GetPlayerCharacterAsync().ConfigureAwait(false));
if (player == null) continue;
WorldData worldData;
if (_dalamudUtil.IsInGpose)
{
player = await _dalamudUtil.GetGposeCharacterFromObjectTableByNameAsync(player.Name.TextValue, true).ConfigureAwait(false);
if (player == null) continue;
worldData = (await _brio.GetTransformAsync(player.Address).ConfigureAwait(false));
}
else
{
var rotQuaternion = Quaternion.CreateFromAxisAngle(new Vector3(0, 1, 0), player.Rotation);
worldData = new()
{
PositionX = player.Position.X,
PositionY = player.Position.Y,
PositionZ = player.Position.Z,
RotationW = rotQuaternion.W,
RotationX = rotQuaternion.X,
RotationY = rotQuaternion.Y,
RotationZ = rotQuaternion.Z,
ScaleX = 1,
ScaleY = 1,
ScaleZ = 1
};
}
var loc = await _dalamudUtil.GetMapDataAsync().ConfigureAwait(false);
worldData.LocationInfo = loc;
if (_forceResendWorldData || worldData != _lastWorldData)
{
_forceResendWorldData = false;
await _apiController.GposeLobbyPushWorldData(worldData).ConfigureAwait(false);
_lastWorldData = worldData;
Logger.LogTrace("WorldData (gpose: {gpose}): {data}", _dalamudUtil.IsInGpose, worldData);
}
foreach (var entry in _usersInLobby)
{
if (!entry.Value.HasWorldDataUpdate || _dalamudUtil.IsInGpose || entry.Value.WorldData == null) continue;
var entryWorldData = entry.Value.WorldData!.Value;
if (worldData.LocationInfo.MapId == entryWorldData.LocationInfo.MapId && worldData.LocationInfo.DivisionId == entryWorldData.LocationInfo.DivisionId
&& (worldData.LocationInfo.HouseId != entryWorldData.LocationInfo.HouseId
|| worldData.LocationInfo.WardId != entryWorldData.LocationInfo.WardId
|| entryWorldData.LocationInfo.ServerId != worldData.LocationInfo.ServerId))
{
if (entry.Value.SpawnedVfxId == null)
{
// spawn if it doesn't exist yet
entry.Value.LastWorldPosition = new Vector3(entryWorldData.PositionX, entryWorldData.PositionY, entryWorldData.PositionZ);
entry.Value.SpawnedVfxId = await _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.SpawnObject(entry.Value.LastWorldPosition.Value,
Quaternion.Identity, Vector3.One, 0.5f, 0.1f, 0.5f, 0.9f)).ConfigureAwait(false);
}
else
{
// move object via lerp if it does exist
var newPosition = new Vector3(entryWorldData.PositionX, entryWorldData.PositionY, entryWorldData.PositionZ);
if (newPosition != entry.Value.LastWorldPosition)
{
entry.Value.UpdateStart = DateTime.UtcNow;
entry.Value.TargetWorldPosition = newPosition;
}
}
}
else
{
await _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(entry.Value.SpawnedVfxId)).ConfigureAwait(false);
entry.Value.SpawnedVfxId = null;
}
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during World Data Generation");
}
}
}
private void OnCutsceneFrameworkUpdate()
{
foreach (var kvp in _usersInLobby)
{
if (!string.IsNullOrWhiteSpace(kvp.Value.AssociatedCharaName))
{
kvp.Value.Address = _dalamudUtil.GetGposeCharacterFromObjectTableByName(kvp.Value.AssociatedCharaName, true)?.Address ?? nint.Zero;
if (kvp.Value.Address == nint.Zero)
{
kvp.Value.AssociatedCharaName = string.Empty;
}
}
if (kvp.Value.Address != nint.Zero && (kvp.Value.HasWorldDataUpdate || kvp.Value.HasPoseDataUpdate))
{
bool hadPoseDataUpdate = kvp.Value.HasPoseDataUpdate;
bool hadWorldDataUpdate = kvp.Value.HasWorldDataUpdate;
kvp.Value.HasPoseDataUpdate = false;
kvp.Value.HasWorldDataUpdate = false;
_ = Task.Run(async () =>
{
if (hadPoseDataUpdate && kvp.Value.ApplicablePoseData != null)
{
await _brio.SetPoseAsync(kvp.Value.Address, CreateJsonFromPoseData(kvp.Value.ApplicablePoseData)).ConfigureAwait(false);
}
if (hadWorldDataUpdate && kvp.Value.WorldData != null)
{
await _brio.ApplyTransformAsync(kvp.Value.Address, kvp.Value.WorldData.Value).ConfigureAwait(false);
}
});
}
}
}
private void OnEnterGpose()
{
ForceResendOwnData();
ResetOwnData();
foreach (var data in _usersInLobby.Values)
{
_ = _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(data.SpawnedVfxId));
data.Reset();
}
}
private void OnExitGpose()
{
ForceResendOwnData();
ResetOwnData();
foreach (var data in _usersInLobby.Values)
{
data.Reset();
}
}
private bool _forceResendFullPose = false;
private bool _forceResendWorldData = false;
private void ForceResendOwnData()
{
_forceResendFullPose = true;
_forceResendWorldData = true;
}
private void ResetOwnData()
{
_poseGenerationExecutions = 0;
_lastCreatedCharaData = null;
}
private void OnFrameworkUpdate()
{
var frameworkTime = DateTime.UtcNow;
foreach (var kvp in _usersInLobby)
{
if (kvp.Value.SpawnedVfxId != null && kvp.Value.UpdateStart != null)
{
var secondsElasped = frameworkTime.Subtract(kvp.Value.UpdateStart.Value).TotalSeconds;
if (secondsElasped >= 1)
{
kvp.Value.LastWorldPosition = kvp.Value.TargetWorldPosition;
kvp.Value.TargetWorldPosition = null;
kvp.Value.UpdateStart = null;
}
else
{
var lerp = Vector3.Lerp(kvp.Value.LastWorldPosition ?? Vector3.One, kvp.Value.TargetWorldPosition ?? Vector3.One, (float)secondsElasped);
_vfxSpawnManager.MoveObject(kvp.Value.SpawnedVfxId.Value, lerp);
}
}
}
}
private void OnReceiveCharaData(CharaDataDownloadDto charaDataDownloadDto)
{
if (!_usersInLobby.TryGetValue(charaDataDownloadDto.Uploader.UID, out var lobbyData))
{
return;
}
lobbyData.CharaData = charaDataDownloadDto;
if (lobbyData.Address != nint.Zero && !string.IsNullOrEmpty(lobbyData.AssociatedCharaName))
{
_ = ApplyCharaData(lobbyData);
}
}
public async Task ApplyCharaData(GposeLobbyUserData userData)
{
if (userData.CharaData == null || userData.Address == nint.Zero || string.IsNullOrEmpty(userData.AssociatedCharaName))
return;
await _charaDataCreationSemaphore.WaitAsync(_lobbyCts.Token).ConfigureAwait(false);
try
{
await _charaDataManager.ApplyCharaData(userData.CharaData!, userData.AssociatedCharaName).ConfigureAwait(false);
userData.LastAppliedCharaDataDate = userData.CharaData.UpdatedDate;
userData.HasPoseDataUpdate = true;
userData.HasWorldDataUpdate = true;
}
finally
{
_charaDataCreationSemaphore.Release();
}
}
private readonly SemaphoreSlim _charaDataSpawnSemaphore = new(1, 1);
internal async Task SpawnAndApplyData(GposeLobbyUserData userData)
{
if (userData.CharaData == null)
return;
await _charaDataSpawnSemaphore.WaitAsync(_lobbyCts.Token).ConfigureAwait(false);
try
{
userData.HasPoseDataUpdate = false;
userData.HasWorldDataUpdate = false;
var chara = await _charaDataManager.SpawnAndApplyData(userData.CharaData).ConfigureAwait(false);
if (chara == null) return;
userData.HandledChara = chara;
userData.AssociatedCharaName = chara.Name;
userData.HasPoseDataUpdate = true;
userData.HasWorldDataUpdate = true;
}
finally
{
_charaDataSpawnSemaphore.Release();
}
}
private void OnReceivePoseData(UserData userData, PoseData poseData)
{
if (!_usersInLobby.TryGetValue(userData.UID, out var lobbyData))
{
return;
}
if (poseData.IsDelta)
lobbyData.DeltaPoseData = poseData;
else
lobbyData.FullPoseData = poseData;
}
private void OnReceiveWorldData(UserData userData, WorldData worldData)
{
_usersInLobby[userData.UID].WorldData = worldData;
_ = _usersInLobby[userData.UID].SetWorldDataDescriptor(_dalamudUtil);
}
private void OnUserJoinLobby(UserData userData)
{
if (_usersInLobby.ContainsKey(userData.UID))
OnUserLeaveLobby(userData);
_usersInLobby[userData.UID] = new(userData);
_ = PushCharacterDownloadDto();
}
private void OnUserLeaveLobby(UserData msg)
{
_usersInLobby.Remove(msg.UID, out var existingData);
if (existingData != default)
{
_ = _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(existingData.SpawnedVfxId));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,296 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using MareSynchronos.API.Data;
using MareSynchronos.Interop;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.CharaData.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace MareSynchronos.Services;
public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
{
public record NearbyCharaDataEntry
{
public float Direction { get; init; }
public float Distance { get; init; }
}
private readonly DalamudUtilService _dalamudUtilService;
private readonly Dictionary<PoseEntryExtended, NearbyCharaDataEntry> _nearbyData = [];
private readonly Dictionary<PoseEntryExtended, Guid> _poseVfx = [];
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly CharaDataConfigService _charaDataConfigService;
private readonly Dictionary<UserData, List<CharaDataMetaInfoExtendedDto>> _metaInfoCache = [];
private readonly VfxSpawnManager _vfxSpawnManager;
private Task? _filterEntriesRunningTask;
private (Guid VfxId, PoseEntryExtended Pose)? _hoveredVfx = null;
private DateTime _lastExecutionTime = DateTime.UtcNow;
private SemaphoreSlim _sharedDataUpdateSemaphore = new(1, 1);
public CharaDataNearbyManager(ILogger<CharaDataNearbyManager> logger, MareMediator mediator,
DalamudUtilService dalamudUtilService, VfxSpawnManager vfxSpawnManager,
ServerConfigurationManager serverConfigurationManager,
CharaDataConfigService charaDataConfigService) : base(logger, mediator)
{
mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => HandleFrameworkUpdate());
mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (_) => HandleFrameworkUpdate());
_dalamudUtilService = dalamudUtilService;
_vfxSpawnManager = vfxSpawnManager;
_serverConfigurationManager = serverConfigurationManager;
_charaDataConfigService = charaDataConfigService;
mediator.Subscribe<GposeStartMessage>(this, (_) => ClearAllVfx());
}
public bool ComputeNearbyData { get; set; } = false;
public IDictionary<PoseEntryExtended, NearbyCharaDataEntry> NearbyData => _nearbyData;
public string UserNoteFilter { get; set; } = string.Empty;
public void UpdateSharedData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
{
_sharedDataUpdateSemaphore.Wait();
try
{
_metaInfoCache.Clear();
foreach (var kvp in newData)
{
if (kvp.Value == null) continue;
if (!_metaInfoCache.TryGetValue(kvp.Value.Uploader, out var list))
{
_metaInfoCache[kvp.Value.Uploader] = list = [];
}
list.Add(kvp.Value);
}
}
finally
{
_sharedDataUpdateSemaphore.Release();
}
}
internal void SetHoveredVfx(PoseEntryExtended? hoveredPose)
{
if (hoveredPose == null && _hoveredVfx == null)
return;
if (hoveredPose == null)
{
_vfxSpawnManager.DespawnObject(_hoveredVfx!.Value.VfxId);
_hoveredVfx = null;
return;
}
if (_hoveredVfx == null)
{
var vfxGuid = _vfxSpawnManager.SpawnObject(hoveredPose.Position, hoveredPose.Rotation, Vector3.One * 4, 1, 0.2f, 0.2f, 1f);
if (vfxGuid != null)
_hoveredVfx = (vfxGuid.Value, hoveredPose);
return;
}
if (hoveredPose != _hoveredVfx!.Value.Pose)
{
_vfxSpawnManager.DespawnObject(_hoveredVfx.Value.VfxId);
var vfxGuid = _vfxSpawnManager.SpawnObject(hoveredPose.Position, hoveredPose.Rotation, Vector3.One * 4, 1, 0.2f, 0.2f, 1f);
if (vfxGuid != null)
_hoveredVfx = (vfxGuid.Value, hoveredPose);
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
ClearAllVfx();
}
private static float CalculateYawDegrees(Vector3 directionXZ)
{
// Calculate yaw angle in radians using Atan2 (X, Z)
float yawRadians = (float)Math.Atan2(-directionXZ.X, directionXZ.Z);
float yawDegrees = yawRadians * (180f / (float)Math.PI);
// Normalize to [0, 360)
if (yawDegrees < 0)
yawDegrees += 360f;
return yawDegrees;
}
private static float GetAngleToTarget(Vector3 cameraPosition, float cameraYawDegrees, Vector3 targetPosition)
{
// Step 4: Calculate the direction vector from camera to target
Vector3 directionToTarget = targetPosition - cameraPosition;
// Step 5: Project the directionToTarget onto the XZ plane (ignore Y)
Vector3 directionToTargetXZ = new Vector3(directionToTarget.X, 0, directionToTarget.Z);
// Handle the case where the target is directly above or below the camera
if (directionToTargetXZ.LengthSquared() < 1e-10f)
{
return 0; // Default direction
}
directionToTargetXZ = Vector3.Normalize(directionToTargetXZ);
// Step 6: Calculate the target's yaw angle
float targetYawDegrees = CalculateYawDegrees(directionToTargetXZ);
// Step 7: Calculate relative angle
float relativeAngle = targetYawDegrees - cameraYawDegrees;
if (relativeAngle < 0)
relativeAngle += 360f;
// Step 8: Map relative angle to ArrowDirection
return relativeAngle;
}
private static float GetCameraYaw(Vector3 cameraPosition, Vector3 lookAtVector)
{
// Step 1: Calculate the direction vector from camera to LookAtPoint
Vector3 directionFacing = lookAtVector - cameraPosition;
// Step 2: Project the directionFacing onto the XZ plane (ignore Y)
Vector3 directionFacingXZ = new Vector3(directionFacing.X, 0, directionFacing.Z);
// Handle the case where the LookAtPoint is directly above or below the camera
if (directionFacingXZ.LengthSquared() < 1e-10f)
{
// Default to facing forward along the Z-axis if LookAtPoint is directly above or below
directionFacingXZ = new Vector3(0, 0, 1);
}
else
{
directionFacingXZ = Vector3.Normalize(directionFacingXZ);
}
// Step 3: Calculate the camera's yaw angle based on directionFacingXZ
return (CalculateYawDegrees(directionFacingXZ));
}
private void ClearAllVfx()
{
foreach (var vfx in _poseVfx)
{
_vfxSpawnManager.DespawnObject(vfx.Value);
}
_poseVfx.Clear();
}
private async Task FilterEntriesAsync(Vector3 cameraPos, Vector3 cameraLookAt)
{
var previousPoses = _nearbyData.Keys.ToList();
_nearbyData.Clear();
var ownLocation = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetMapData()).ConfigureAwait(false);
var player = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetPlayerCharacter()).ConfigureAwait(false);
var currentServer = player.CurrentWorld;
var playerPos = player.Position;
var cameraYaw = GetCameraYaw(cameraPos, cameraLookAt);
bool ignoreHousingLimits = _charaDataConfigService.Current.NearbyIgnoreHousingLimitations;
bool onlyCurrentServer = _charaDataConfigService.Current.NearbyOwnServerOnly;
bool showOwnData = _charaDataConfigService.Current.NearbyShowOwnData;
// initial filter on name
foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter)
|| ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|| d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|| (_serverConfigurationManager.GetNoteForUid(UserNoteFilter) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
.ToDictionary(k => k.Key, k => k.Value))
{
// filter all poses based on territory, that always must be correct
foreach (var pose in data.Value.Where(v => v.HasPoses && v.HasWorldData && (showOwnData || !v.IsOwnData))
.SelectMany(k => k.PoseExtended)
.Where(p => p.HasPoseData
&& p.HasWorldData
&& p.WorldData!.Value.LocationInfo.TerritoryId == ownLocation.TerritoryId)
.ToList())
{
var poseLocation = pose.WorldData!.Value.LocationInfo;
bool isInHousing = poseLocation.WardId != 0;
var distance = Vector3.Distance(playerPos, pose.Position);
if (distance > _charaDataConfigService.Current.NearbyDistanceFilter) continue;
bool addEntry = (!isInHousing && poseLocation.MapId == ownLocation.MapId
&& (!onlyCurrentServer || poseLocation.ServerId == currentServer.RowId))
|| (isInHousing
&& (((ignoreHousingLimits && !onlyCurrentServer)
|| (ignoreHousingLimits && onlyCurrentServer) && poseLocation.ServerId == currentServer.RowId)
|| poseLocation.ServerId == currentServer.RowId)
&& ((poseLocation.HouseId == 0 && poseLocation.DivisionId == ownLocation.DivisionId
&& (ignoreHousingLimits || poseLocation.WardId == ownLocation.WardId))
|| (poseLocation.HouseId > 0
&& (ignoreHousingLimits || (poseLocation.HouseId == ownLocation.HouseId && poseLocation.WardId == ownLocation.WardId && poseLocation.DivisionId == ownLocation.DivisionId && poseLocation.RoomId == ownLocation.RoomId)))
));
if (addEntry)
_nearbyData[pose] = new() { Direction = GetAngleToTarget(cameraPos, cameraYaw, pose.Position), Distance = distance };
}
}
if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombatOrPerforming)
await _dalamudUtilService.RunOnFrameworkThread(() => ManageWispsNearby(previousPoses)).ConfigureAwait(false);
}
private unsafe void HandleFrameworkUpdate()
{
if (_lastExecutionTime.AddSeconds(0.5) > DateTime.UtcNow) return;
_lastExecutionTime = DateTime.UtcNow;
if (!ComputeNearbyData && !_charaDataConfigService.Current.NearbyShowAlways)
{
if (_nearbyData.Any())
_nearbyData.Clear();
if (_poseVfx.Any())
ClearAllVfx();
return;
}
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombatOrPerforming)
ClearAllVfx();
var camera = CameraManager.Instance()->CurrentCamera;
Vector3 cameraPos = new(camera->Position.X, camera->Position.Y, camera->Position.Z);
Vector3 lookAt = new(camera->LookAtVector.X, camera->LookAtVector.Y, camera->LookAtVector.Z);
if (_filterEntriesRunningTask?.IsCompleted ?? true && _dalamudUtilService.IsLoggedIn)
_filterEntriesRunningTask = FilterEntriesAsync(cameraPos, lookAt);
}
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses)
{
foreach (var data in _nearbyData.Keys)
{
if (_poseVfx.TryGetValue(data, out var _)) continue;
Guid? vfxGuid;
if (data.MetaInfo.IsOwnData)
{
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f);
}
else
{
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
}
if (vfxGuid != null)
{
_poseVfx[data] = vfxGuid.Value;
}
}
foreach (var data in previousPoses.Except(_nearbyData.Keys))
{
if (_poseVfx.Remove(data, out var guid))
{
_vfxSpawnManager.DespawnObject(guid);
}
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MareSynchronos.Services.CharaData
{
internal class CharaDataTogetherManager
{
}
}

View File

@@ -0,0 +1,20 @@
using MareSynchronos.API.Data;
using MareSynchronos.FileCache;
using MareSynchronos.Services.CharaData.Models;
namespace MareSynchronos.Services.CharaData;
public sealed class MareCharaFileDataFactory
{
private readonly FileCacheManager _fileCacheManager;
public MareCharaFileDataFactory(FileCacheManager fileCacheManager)
{
_fileCacheManager = fileCacheManager;
}
public MareCharaFileData Create(string description, CharacterData characterCacheDto)
{
return new MareCharaFileData(_fileCacheManager, description, characterCacheDto);
}
}

View File

@@ -0,0 +1,354 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.CharaData;
namespace MareSynchronos.Services.CharaData.Models;
public sealed record CharaDataExtendedUpdateDto : CharaDataUpdateDto
{
private readonly CharaDataFullDto _charaDataFullDto;
public CharaDataExtendedUpdateDto(CharaDataUpdateDto dto, CharaDataFullDto charaDataFullDto) : base(dto)
{
_charaDataFullDto = charaDataFullDto;
_userList = charaDataFullDto.AllowedUsers.ToList();
_groupList = charaDataFullDto.AllowedGroups.ToList();
_poseList = charaDataFullDto.PoseData.Select(k => new PoseEntry(k.Id)
{
Description = k.Description,
PoseData = k.PoseData,
WorldData = k.WorldData
}).ToList();
}
public CharaDataUpdateDto BaseDto => new(Id)
{
AllowedUsers = AllowedUsers,
AllowedGroups = AllowedGroups,
AccessType = base.AccessType,
CustomizeData = base.CustomizeData,
Description = base.Description,
ExpiryDate = base.ExpiryDate,
FileGamePaths = base.FileGamePaths,
FileSwaps = base.FileSwaps,
GlamourerData = base.GlamourerData,
ShareType = base.ShareType,
ManipulationData = base.ManipulationData,
Poses = Poses
};
public new string ManipulationData
{
get
{
return base.ManipulationData ?? _charaDataFullDto.ManipulationData;
}
set
{
base.ManipulationData = value;
if (string.Equals(base.ManipulationData, _charaDataFullDto.ManipulationData, StringComparison.Ordinal))
{
base.ManipulationData = null;
}
}
}
public new string Description
{
get
{
return base.Description ?? _charaDataFullDto.Description;
}
set
{
base.Description = value;
if (string.Equals(base.Description, _charaDataFullDto.Description, StringComparison.Ordinal))
{
base.Description = null;
}
}
}
public new DateTime ExpiryDate
{
get
{
return base.ExpiryDate ?? _charaDataFullDto.ExpiryDate;
}
private set
{
base.ExpiryDate = value;
if (Equals(base.ExpiryDate, _charaDataFullDto.ExpiryDate))
{
base.ExpiryDate = null;
}
}
}
public new AccessTypeDto AccessType
{
get
{
return base.AccessType ?? _charaDataFullDto.AccessType;
}
set
{
base.AccessType = value;
if (Equals(base.AccessType, _charaDataFullDto.AccessType))
{
base.AccessType = null;
}
}
}
public new ShareTypeDto ShareType
{
get
{
return base.ShareType ?? _charaDataFullDto.ShareType;
}
set
{
base.ShareType = value;
if (Equals(base.ShareType, _charaDataFullDto.ShareType))
{
base.ShareType = null;
}
}
}
public new List<GamePathEntry>? FileGamePaths
{
get
{
return base.FileGamePaths ?? _charaDataFullDto.FileGamePaths;
}
set
{
base.FileGamePaths = value;
if (!(base.FileGamePaths ?? []).Except(_charaDataFullDto.FileGamePaths).Any()
&& !_charaDataFullDto.FileGamePaths.Except(base.FileGamePaths ?? []).Any())
{
base.FileGamePaths = null;
}
}
}
public new List<GamePathEntry>? FileSwaps
{
get
{
return base.FileSwaps ?? _charaDataFullDto.FileSwaps;
}
set
{
base.FileSwaps = value;
if (!(base.FileSwaps ?? []).Except(_charaDataFullDto.FileSwaps).Any()
&& !_charaDataFullDto.FileSwaps.Except(base.FileSwaps ?? []).Any())
{
base.FileSwaps = null;
}
}
}
public new string? GlamourerData
{
get
{
return base.GlamourerData ?? _charaDataFullDto.GlamourerData;
}
set
{
base.GlamourerData = value;
if (string.Equals(base.GlamourerData, _charaDataFullDto.GlamourerData, StringComparison.Ordinal))
{
base.GlamourerData = null;
}
}
}
public new string? CustomizeData
{
get
{
return base.CustomizeData ?? _charaDataFullDto.CustomizeData;
}
set
{
base.CustomizeData = value;
if (string.Equals(base.CustomizeData, _charaDataFullDto.CustomizeData, StringComparison.Ordinal))
{
base.CustomizeData = null;
}
}
}
public IEnumerable<UserData> UserList => _userList;
private readonly List<UserData> _userList;
public IEnumerable<GroupData> GroupList => _groupList;
private readonly List<GroupData> _groupList;
public IEnumerable<PoseEntry> PoseList => _poseList;
private readonly List<PoseEntry> _poseList;
public void AddUserToList(string user)
{
_userList.Add(new(user, null));
UpdateAllowedUsers();
}
public void AddGroupToList(string group)
{
_groupList.Add(new(group, null));
UpdateAllowedGroups();
}
private void UpdateAllowedUsers()
{
AllowedUsers = [.. _userList.Select(u => u.UID)];
if (!AllowedUsers.Except(_charaDataFullDto.AllowedUsers.Select(u => u.UID), StringComparer.Ordinal).Any()
&& !_charaDataFullDto.AllowedUsers.Select(u => u.UID).Except(AllowedUsers, StringComparer.Ordinal).Any())
{
AllowedUsers = null;
}
}
private void UpdateAllowedGroups()
{
AllowedGroups = [.. _groupList.Select(u => u.GID)];
if (!AllowedGroups.Except(_charaDataFullDto.AllowedGroups.Select(u => u.GID), StringComparer.Ordinal).Any()
&& !_charaDataFullDto.AllowedGroups.Select(u => u.GID).Except(AllowedGroups, StringComparer.Ordinal).Any())
{
AllowedGroups = null;
}
}
public void RemoveUserFromList(string user)
{
_userList.RemoveAll(u => string.Equals(u.UID, user, StringComparison.Ordinal));
UpdateAllowedUsers();
}
public void RemoveGroupFromList(string group)
{
_groupList.RemoveAll(u => string.Equals(u.GID, group, StringComparison.Ordinal));
UpdateAllowedGroups();
}
public void AddPose()
{
_poseList.Add(new PoseEntry(null));
UpdatePoseList();
}
public void RemovePose(PoseEntry entry)
{
if (entry.Id != null)
{
entry.Description = null;
entry.WorldData = null;
entry.PoseData = null;
}
else
{
_poseList.Remove(entry);
}
UpdatePoseList();
}
public void UpdatePoseList()
{
Poses = [.. _poseList];
if (!Poses.Except(_charaDataFullDto.PoseData).Any() && !_charaDataFullDto.PoseData.Except(Poses).Any())
{
Poses = null;
}
}
public void SetExpiry(bool expiring)
{
if (expiring)
{
var date = DateTime.UtcNow.AddDays(7);
SetExpiry(date.Year, date.Month, date.Day);
}
else
{
ExpiryDate = DateTime.MaxValue;
}
}
public void SetExpiry(int year, int month, int day)
{
int daysInMonth = DateTime.DaysInMonth(year, month);
if (day > daysInMonth) day = 1;
ExpiryDate = new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
internal void UndoChanges()
{
base.Description = null;
base.AccessType = null;
base.ShareType = null;
base.GlamourerData = null;
base.FileSwaps = null;
base.FileGamePaths = null;
base.CustomizeData = null;
base.ManipulationData = null;
AllowedUsers = null;
AllowedGroups = null;
Poses = null;
_poseList.Clear();
_poseList.AddRange(_charaDataFullDto.PoseData.Select(k => new PoseEntry(k.Id)
{
Description = k.Description,
PoseData = k.PoseData,
WorldData = k.WorldData
}));
}
internal void RevertDeletion(PoseEntry pose)
{
if (pose.Id == null) return;
var oldPose = _charaDataFullDto.PoseData.Find(p => p.Id == pose.Id);
if (oldPose == null) return;
pose.Description = oldPose.Description;
pose.PoseData = oldPose.PoseData;
pose.WorldData = oldPose.WorldData;
UpdatePoseList();
}
internal bool PoseHasChanges(PoseEntry pose)
{
if (pose.Id == null) return false;
var oldPose = _charaDataFullDto.PoseData.Find(p => p.Id == pose.Id);
if (oldPose == null) return false;
return !string.Equals(pose.Description, oldPose.Description, StringComparison.Ordinal)
|| !string.Equals(pose.PoseData, oldPose.PoseData, StringComparison.Ordinal)
|| pose.WorldData != oldPose.WorldData;
}
public bool HasChanges =>
base.Description != null
|| base.ExpiryDate != null
|| base.AccessType != null
|| base.ShareType != null
|| AllowedUsers != null
|| AllowedGroups != null
|| base.GlamourerData != null
|| base.FileSwaps != null
|| base.FileGamePaths != null
|| base.CustomizeData != null
|| base.ManipulationData != null
|| Poses != null;
public bool IsAppearanceEqual =>
string.Equals(GlamourerData, _charaDataFullDto.GlamourerData, StringComparison.Ordinal)
&& string.Equals(CustomizeData, _charaDataFullDto.CustomizeData, StringComparison.Ordinal)
&& FileGamePaths == _charaDataFullDto.FileGamePaths
&& FileSwaps == _charaDataFullDto.FileSwaps
&& string.Equals(ManipulationData, _charaDataFullDto.ManipulationData, StringComparison.Ordinal);
}

View File

@@ -0,0 +1,18 @@
using MareSynchronos.API.Dto.CharaData;
using System.Collections.ObjectModel;
namespace MareSynchronos.Services.CharaData.Models;
public sealed record CharaDataFullExtendedDto : CharaDataFullDto
{
public CharaDataFullExtendedDto(CharaDataFullDto baseDto) : base(baseDto)
{
FullId = baseDto.Uploader.UID + ":" + baseDto.Id;
MissingFiles = new ReadOnlyCollection<GamePathEntry>(baseDto.OriginalFiles.Except(baseDto.FileGamePaths).ToList());
HasMissingFiles = MissingFiles.Any();
}
public string FullId { get; set; }
public bool HasMissingFiles { get; init; }
public IReadOnlyCollection<GamePathEntry> MissingFiles { get; init; }
}

View File

@@ -0,0 +1,31 @@
using MareSynchronos.API.Dto.CharaData;
namespace MareSynchronos.Services.CharaData.Models;
public sealed record CharaDataMetaInfoExtendedDto : CharaDataMetaInfoDto
{
private CharaDataMetaInfoExtendedDto(CharaDataMetaInfoDto baseMeta) : base(baseMeta)
{
FullId = baseMeta.Uploader.UID + ":" + baseMeta.Id;
}
public List<PoseEntryExtended> PoseExtended { get; private set; } = [];
public bool HasPoses => PoseExtended.Count != 0;
public bool HasWorldData => PoseExtended.Exists(p => p.HasWorldData);
public bool IsOwnData { get; private set; }
public string FullId { get; private set; }
public async static Task<CharaDataMetaInfoExtendedDto> Create(CharaDataMetaInfoDto baseMeta, DalamudUtilService dalamudUtilService, bool isOwnData = false)
{
CharaDataMetaInfoExtendedDto newDto = new(baseMeta);
foreach (var pose in newDto.PoseData)
{
newDto.PoseExtended.Add(await PoseEntryExtended.Create(pose, newDto, dalamudUtilService).ConfigureAwait(false));
}
newDto.IsOwnData = isOwnData;
return newDto;
}
}

View File

@@ -0,0 +1,174 @@
using Dalamud.Utility;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.Utils;
using System.Globalization;
using System.Numerics;
using System.Text;
namespace MareSynchronos.Services.CharaData.Models;
public sealed record GposeLobbyUserData(UserData UserData)
{
public void Reset()
{
HasWorldDataUpdate = WorldData != null;
HasPoseDataUpdate = ApplicablePoseData != null;
SpawnedVfxId = null;
LastAppliedCharaDataDate = DateTime.MinValue;
}
private WorldData? _worldData;
public WorldData? WorldData
{
get => _worldData; set
{
_worldData = value;
HasWorldDataUpdate = true;
}
}
public bool HasWorldDataUpdate { get; set; } = false;
private PoseData? _fullPoseData;
private PoseData? _deltaPoseData;
public PoseData? FullPoseData
{
get => _fullPoseData;
set
{
_fullPoseData = value;
ApplicablePoseData = CombinePoseData();
HasPoseDataUpdate = true;
}
}
public PoseData? DeltaPoseData
{
get => _deltaPoseData;
set
{
_deltaPoseData = value;
ApplicablePoseData = CombinePoseData();
HasPoseDataUpdate = true;
}
}
public PoseData? ApplicablePoseData { get; private set; }
public bool HasPoseDataUpdate { get; set; } = false;
public Guid? SpawnedVfxId { get; set; }
public Vector3? LastWorldPosition { get; set; }
public Vector3? TargetWorldPosition { get; set; }
public DateTime? UpdateStart { get; set; }
private CharaDataDownloadDto? _charaData;
public CharaDataDownloadDto? CharaData
{
get => _charaData; set
{
_charaData = value;
LastUpdatedCharaData = _charaData?.UpdatedDate ?? DateTime.MaxValue;
}
}
public DateTime LastUpdatedCharaData { get; private set; } = DateTime.MaxValue;
public DateTime LastAppliedCharaDataDate { get; set; } = DateTime.MinValue;
public nint Address { get; set; }
public string AssociatedCharaName { get; set; } = string.Empty;
private PoseData? CombinePoseData()
{
if (DeltaPoseData == null && FullPoseData != null) return FullPoseData;
if (FullPoseData == null) return null;
PoseData output = FullPoseData!.Value.DeepClone();
PoseData delta = DeltaPoseData!.Value;
foreach (var bone in FullPoseData!.Value.Bones)
{
if (!delta.Bones.TryGetValue(bone.Key, out var data)) continue;
if (!data.Exists)
{
output.Bones.Remove(bone.Key);
}
else
{
output.Bones[bone.Key] = data;
}
}
foreach (var bone in FullPoseData!.Value.MainHand)
{
if (!delta.MainHand.TryGetValue(bone.Key, out var data)) continue;
if (!data.Exists)
{
output.MainHand.Remove(bone.Key);
}
else
{
output.MainHand[bone.Key] = data;
}
}
foreach (var bone in FullPoseData!.Value.OffHand)
{
if (!delta.OffHand.TryGetValue(bone.Key, out var data)) continue;
if (!data.Exists)
{
output.OffHand.Remove(bone.Key);
}
else
{
output.OffHand[bone.Key] = data;
}
}
return output;
}
public string WorldDataDescriptor { get; private set; } = string.Empty;
public Vector2 MapCoordinates { get; private set; }
public Lumina.Excel.Sheets.Map Map { get; private set; }
public HandledCharaDataEntry? HandledChara { get; set; }
public async Task SetWorldDataDescriptor(DalamudUtilService dalamudUtilService)
{
if (WorldData == null)
{
WorldDataDescriptor = "No World Data found";
}
var worldData = WorldData!.Value;
MapCoordinates = await dalamudUtilService.RunOnFrameworkThread(() =>
MapUtil.WorldToMap(new Vector2(worldData.PositionX, worldData.PositionY), dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map))
.ConfigureAwait(false);
Map = dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map;
StringBuilder sb = new();
sb.AppendLine("Server: " + dalamudUtilService.WorldData.Value[(ushort)worldData.LocationInfo.ServerId]);
sb.AppendLine("Territory: " + dalamudUtilService.TerritoryData.Value[worldData.LocationInfo.TerritoryId]);
sb.AppendLine("Map: " + dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].MapName);
if (worldData.LocationInfo.WardId != 0)
sb.AppendLine("Ward #: " + worldData.LocationInfo.WardId);
if (worldData.LocationInfo.DivisionId != 0)
{
sb.AppendLine("Subdivision: " + worldData.LocationInfo.DivisionId switch
{
1 => "No",
2 => "Yes",
_ => "-"
});
}
if (worldData.LocationInfo.HouseId != 0)
{
sb.AppendLine("House #: " + (worldData.LocationInfo.HouseId == 100 ? "Apartments" : worldData.LocationInfo.HouseId.ToString()));
}
if (worldData.LocationInfo.RoomId != 0)
{
sb.AppendLine("Apartment #: " + worldData.LocationInfo.RoomId);
}
sb.AppendLine("Coordinates: X: " + MapCoordinates.X.ToString("0.0", CultureInfo.InvariantCulture) + ", Y: " + MapCoordinates.Y.ToString("0.0", CultureInfo.InvariantCulture));
WorldDataDescriptor = sb.ToString();
}
}

View File

@@ -0,0 +1,6 @@
namespace MareSynchronos.Services.CharaData.Models;
public sealed record HandledCharaDataEntry(string Name, bool IsSelf, Guid? CustomizePlus, CharaDataMetaInfoExtendedDto MetaInfo)
{
public CharaDataMetaInfoExtendedDto MetaInfo { get; set; } = MetaInfo;
}

View File

@@ -0,0 +1,70 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.FileCache;
using System.Text;
using System.Text.Json;
namespace MareSynchronos.Services.CharaData.Models;
public record MareCharaFileData
{
public string Description { get; set; } = string.Empty;
public string GlamourerData { get; set; } = string.Empty;
public string CustomizePlusData { get; set; } = string.Empty;
public string ManipulationData { get; set; } = string.Empty;
public List<FileData> Files { get; set; } = [];
public List<FileSwap> FileSwaps { get; set; } = [];
public MareCharaFileData() { }
public MareCharaFileData(FileCacheManager manager, string description, CharacterData dto)
{
Description = description;
if (dto.GlamourerData.TryGetValue(ObjectKind.Player, out var glamourerData))
{
GlamourerData = glamourerData;
}
dto.CustomizePlusData.TryGetValue(ObjectKind.Player, out var customizePlusData);
CustomizePlusData = customizePlusData ?? string.Empty;
ManipulationData = dto.ManipulationData;
if (dto.FileReplacements.TryGetValue(ObjectKind.Player, out var fileReplacements))
{
var grouped = fileReplacements.GroupBy(f => f.Hash, StringComparer.OrdinalIgnoreCase);
foreach (var file in grouped)
{
if (string.IsNullOrEmpty(file.Key))
{
foreach (var item in file)
{
FileSwaps.Add(new FileSwap(item.GamePaths, item.FileSwapPath));
}
}
else
{
var filePath = manager.GetFileCacheByHash(file.First().Hash)?.ResolvedFilepath;
if (filePath != null)
{
Files.Add(new FileData(file.SelectMany(f => f.GamePaths), (int)new FileInfo(filePath).Length, file.First().Hash));
}
}
}
}
}
public byte[] ToByteArray()
{
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(this));
}
public static MareCharaFileData FromByteArray(byte[] data)
{
return JsonSerializer.Deserialize<MareCharaFileData>(Encoding.UTF8.GetString(data))!;
}
public record FileSwap(IEnumerable<string> GamePaths, string FileSwapPath);
public record FileData(IEnumerable<string> GamePaths, int Length, string Hash);
}

View File

@@ -0,0 +1,54 @@
namespace MareSynchronos.Services.CharaData.Models;
public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData)
{
public static readonly byte CurrentVersion = 1;
public byte Version { get; set; } = Version;
public MareCharaFileData CharaFileData { get; set; } = CharaFileData;
public string FilePath { get; private set; } = string.Empty;
public void WriteToStream(BinaryWriter writer)
{
writer.Write('M');
writer.Write('C');
writer.Write('D');
writer.Write('F');
writer.Write(Version);
var charaFileDataArray = CharaFileData.ToByteArray();
writer.Write(charaFileDataArray.Length);
writer.Write(charaFileDataArray);
}
public static MareCharaFileHeader? FromBinaryReader(string path, BinaryReader reader)
{
var chars = new string(reader.ReadChars(4));
if (!string.Equals(chars, "MCDF", StringComparison.Ordinal)) throw new InvalidDataException("Not a Mare Chara File");
MareCharaFileHeader? decoded = null;
var version = reader.ReadByte();
if (version == 1)
{
var dataLength = reader.ReadInt32();
decoded = new(version, MareCharaFileData.FromByteArray(reader.ReadBytes(dataLength)))
{
FilePath = path,
};
}
return decoded;
}
public static void AdvanceReaderToData(BinaryReader reader)
{
reader.ReadChars(4);
var version = reader.ReadByte();
if (version == 1)
{
var length = reader.ReadInt32();
_ = reader.ReadBytes(length);
}
}
}

View File

@@ -0,0 +1,75 @@
using Dalamud.Utility;
using Lumina.Excel.Sheets;
using MareSynchronos.API.Dto.CharaData;
using System.Globalization;
using System.Numerics;
using System.Text;
namespace MareSynchronos.Services.CharaData.Models;
public sealed record PoseEntryExtended : PoseEntry
{
private PoseEntryExtended(PoseEntry basePose, CharaDataMetaInfoExtendedDto parent) : base(basePose)
{
HasPoseData = !string.IsNullOrEmpty(basePose.PoseData);
HasWorldData = (WorldData ?? default) != default;
if (HasWorldData)
{
Position = new(basePose.WorldData!.Value.PositionX, basePose.WorldData!.Value.PositionY, basePose.WorldData!.Value.PositionZ);
Rotation = new(basePose.WorldData!.Value.RotationX, basePose.WorldData!.Value.RotationY, basePose.WorldData!.Value.RotationZ, basePose.WorldData!.Value.RotationW);
}
MetaInfo = parent;
}
public CharaDataMetaInfoExtendedDto MetaInfo { get; }
public bool HasPoseData { get; }
public bool HasWorldData { get; }
public Vector3 Position { get; } = new();
public Vector2 MapCoordinates { get; private set; } = new();
public Quaternion Rotation { get; } = new();
public Map Map { get; private set; }
public string WorldDataDescriptor { get; private set; } = string.Empty;
public static async Task<PoseEntryExtended> Create(PoseEntry baseEntry, CharaDataMetaInfoExtendedDto parent, DalamudUtilService dalamudUtilService)
{
PoseEntryExtended newPose = new(baseEntry, parent);
if (newPose.HasWorldData)
{
var worldData = newPose.WorldData!.Value;
newPose.MapCoordinates = await dalamudUtilService.RunOnFrameworkThread(() =>
MapUtil.WorldToMap(new Vector2(worldData.PositionX, worldData.PositionY), dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map))
.ConfigureAwait(false);
newPose.Map = dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map;
StringBuilder sb = new();
sb.AppendLine("Server: " + dalamudUtilService.WorldData.Value[(ushort)worldData.LocationInfo.ServerId]);
sb.AppendLine("Territory: " + dalamudUtilService.TerritoryData.Value[worldData.LocationInfo.TerritoryId]);
sb.AppendLine("Map: " + dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].MapName);
if (worldData.LocationInfo.WardId != 0)
sb.AppendLine("Ward #: " + worldData.LocationInfo.WardId);
if (worldData.LocationInfo.DivisionId != 0)
{
sb.AppendLine("Subdivision: " + worldData.LocationInfo.DivisionId switch
{
1 => "No",
2 => "Yes",
_ => "-"
});
}
if (worldData.LocationInfo.HouseId != 0)
{
sb.AppendLine("House #: " + (worldData.LocationInfo.HouseId == 100 ? "Apartments" : worldData.LocationInfo.HouseId.ToString()));
}
if (worldData.LocationInfo.RoomId != 0)
{
sb.AppendLine("Apartment #: " + worldData.LocationInfo.RoomId);
}
sb.AppendLine("Coordinates: X: " + newPose.MapCoordinates.X.ToString("0.0", CultureInfo.InvariantCulture) + ", Y: " + newPose.MapCoordinates.Y.ToString("0.0", CultureInfo.InvariantCulture));
newPose.WorldDataDescriptor = sb.ToString();
}
return newPose;
}
}

View File

@@ -0,0 +1,235 @@
using Lumina.Data.Files;
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.FileCache;
using MareSynchronos.Services.Mediator;
using MareSynchronos.UI;
using MareSynchronos.Utils;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{
private readonly FileCacheManager _fileCacheManager;
private readonly XivDataAnalyzer _xivDataAnalyzer;
private CancellationTokenSource? _analysisCts;
private CancellationTokenSource _baseAnalysisCts = new();
private string _lastDataHash = string.Empty;
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
: base(logger, mediator)
{
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
{
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
var token = _baseAnalysisCts.Token;
_ = BaseAnalysis(msg.CharacterData, token);
});
_fileCacheManager = fileCacheManager;
_xivDataAnalyzer = modelAnalyzer;
}
public int CurrentFile { get; internal set; }
public bool IsAnalysisRunning => _analysisCts != null;
public int TotalFiles { get; internal set; }
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
public void CancelAnalyze()
{
_analysisCts?.CancelDispose();
_analysisCts = null;
}
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
{
Logger.LogDebug("=== Calculating Character Analysis ===");
_analysisCts = _analysisCts?.CancelRecreate() ?? new();
var cancelToken = _analysisCts.Token;
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
if (allFiles.Exists(c => !c.IsComputed || recalculate))
{
var remaining = allFiles.Where(c => !c.IsComputed || recalculate).ToList();
TotalFiles = remaining.Count;
CurrentFile = 1;
Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count);
Mediator.Publish(new HaltScanMessage(nameof(CharacterAnalyzer)));
try
{
foreach (var file in remaining)
{
Logger.LogDebug("Computing file {file}", file.FilePaths[0]);
await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false);
CurrentFile++;
}
_fileCacheManager.WriteOutFullCsv();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to analyze files");
}
finally
{
Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer)));
}
}
Mediator.Publish(new CharacterDataAnalyzedMessage());
_analysisCts.CancelDispose();
_analysisCts = null;
if (print) PrintAnalysis();
}
public void Dispose()
{
_analysisCts.CancelDispose();
}
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
{
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
LastAnalysis.Clear();
foreach (var obj in charaData.FileReplacements)
{
Dictionary<string, FileDataEntry> data = new(StringComparer.OrdinalIgnoreCase);
foreach (var fileEntry in obj.Value)
{
token.ThrowIfCancellationRequested();
var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash, ignoreCacheEntries: true, validate: false).ToList();
if (fileCacheEntries.Count == 0) continue;
var filePath = fileCacheEntries[0].ResolvedFilepath;
FileInfo fi = new(filePath);
string ext = "unk?";
try
{
ext = fi.Extension[1..];
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Could not identify extension for {path}", filePath);
}
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
foreach (var entry in fileCacheEntries)
{
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext,
[.. fileEntry.GamePaths],
fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct().ToList(),
entry.Size > 0 ? entry.Size.Value : 0,
entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0,
tris);
}
}
LastAnalysis[obj.Key] = data;
}
Mediator.Publish(new CharacterDataAnalyzedMessage());
_lastDataHash = charaData.DataHash.Value;
}
private void PrintAnalysis()
{
if (LastAnalysis.Count == 0) return;
foreach (var kvp in LastAnalysis)
{
int fileCounter = 1;
int totalFiles = kvp.Value.Count;
Logger.LogInformation("=== Analysis for {obj} ===", kvp.Key);
foreach (var entry in kvp.Value.OrderBy(b => b.Value.GamePaths.OrderBy(p => p, StringComparer.Ordinal).First(), StringComparer.Ordinal))
{
Logger.LogInformation("File {x}/{y}: {hash}", fileCounter++, totalFiles, entry.Key);
foreach (var path in entry.Value.GamePaths)
{
Logger.LogInformation(" Game Path: {path}", path);
}
if (entry.Value.FilePaths.Count > 1) Logger.LogInformation(" Multiple fitting files detected for {key}", entry.Key);
foreach (var filePath in entry.Value.FilePaths)
{
Logger.LogInformation(" File Path: {path}", filePath);
}
Logger.LogInformation(" Size: {size}, Compressed: {compressed}", UiSharedService.ByteToString(entry.Value.OriginalSize),
UiSharedService.ByteToString(entry.Value.CompressedSize));
}
}
foreach (var kvp in LastAnalysis)
{
Logger.LogInformation("=== Detailed summary by file type for {obj} ===", kvp.Key);
foreach (var entry in kvp.Value.Select(v => v.Value).GroupBy(v => v.FileType, StringComparer.Ordinal))
{
Logger.LogInformation("{ext} files: {count}, size extracted: {size}, size compressed: {sizeComp}", entry.Key, entry.Count(),
UiSharedService.ByteToString(entry.Sum(v => v.OriginalSize)), UiSharedService.ByteToString(entry.Sum(v => v.CompressedSize)));
}
Logger.LogInformation("=== Total summary for {obj} ===", kvp.Key);
Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}", kvp.Value.Count,
UiSharedService.ByteToString(kvp.Value.Sum(v => v.Value.OriginalSize)), UiSharedService.ByteToString(kvp.Value.Sum(v => v.Value.CompressedSize)));
}
Logger.LogInformation("=== Total summary for all currently present objects ===");
Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}",
LastAnalysis.Values.Sum(v => v.Values.Count),
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.OriginalSize))),
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
Logger.LogInformation("IMPORTANT NOTES:\n\r- For Mare up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
}
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
{
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token)
{
var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false);
var normalSize = new FileInfo(FilePaths[0]).Length;
var entries = fileCacheManager.GetAllFileCachesByHash(Hash, ignoreCacheEntries: true, validate: false);
foreach (var entry in entries)
{
entry.Size = normalSize;
entry.CompressedSize = compressedsize.Item2.LongLength;
}
OriginalSize = normalSize;
CompressedSize = compressedsize.Item2.LongLength;
}
public long OriginalSize { get; private set; } = OriginalSize;
public long CompressedSize { get; private set; } = CompressedSize;
public long Triangles { get; private set; } = Triangles;
public Lazy<string> Format = new(() =>
{
switch (FileType)
{
case "tex":
{
try
{
using var stream = new FileStream(FilePaths[0], FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new BinaryReader(stream);
reader.BaseStream.Position = 4;
var format = (TexFile.TextureFormat)reader.ReadInt32();
return format.ToString();
}
catch
{
return "Unknown";
}
}
default:
return string.Empty;
}
});
}
}

View File

@@ -0,0 +1,126 @@
using Dalamud.Game.Command;
using Dalamud.Plugin.Services;
using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI;
using MareSynchronos.WebAPI;
using System.Globalization;
namespace MareSynchronos.Services;
public sealed class CommandManagerService : IDisposable
{
private const string _commandName = "/mare";
private readonly ApiController _apiController;
private readonly ICommandManager _commandManager;
private readonly MareMediator _mediator;
private readonly MareConfigService _mareConfigService;
private readonly PerformanceCollectorService _performanceCollectorService;
private readonly CacheMonitor _cacheMonitor;
private readonly ServerConfigurationManager _serverConfigurationManager;
public CommandManagerService(ICommandManager commandManager, PerformanceCollectorService performanceCollectorService,
ServerConfigurationManager serverConfigurationManager, CacheMonitor periodicFileScanner,
ApiController apiController, MareMediator mediator, MareConfigService mareConfigService)
{
_commandManager = commandManager;
_performanceCollectorService = performanceCollectorService;
_serverConfigurationManager = serverConfigurationManager;
_cacheMonitor = periodicFileScanner;
_apiController = apiController;
_mediator = mediator;
_mareConfigService = mareConfigService;
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
{
HelpMessage = "Opens the Mare Synchronos UI" + Environment.NewLine + Environment.NewLine +
"Additionally possible commands:" + Environment.NewLine +
"\t /mare toggle - Disconnects from Mare, if connected. Connects to Mare, if disconnected" + Environment.NewLine +
"\t /mare toggle on|off - Connects or disconnects to Mare respectively" + Environment.NewLine +
"\t /mare gpose - Opens the Mare Character Data Hub window" + Environment.NewLine +
"\t /mare analyze - Opens the Mare Character Data Analysis window" + Environment.NewLine +
"\t /mare settings - Opens the Mare Settings window"
});
}
public void Dispose()
{
_commandManager.RemoveHandler(_commandName);
}
private void OnCommand(string command, string args)
{
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
if (splitArgs.Length == 0)
{
// Interpret this as toggling the UI
if (_mareConfigService.Current.HasValidSetup())
_mediator.Publish(new UiToggleMessage(typeof(CompactUi)));
else
_mediator.Publish(new UiToggleMessage(typeof(IntroUi)));
return;
}
if (!_mareConfigService.Current.HasValidSetup())
return;
if (string.Equals(splitArgs[0], "toggle", StringComparison.OrdinalIgnoreCase))
{
if (_apiController.ServerState == WebAPI.SignalR.Utils.ServerState.Disconnecting)
{
_mediator.Publish(new NotificationMessage("Mare disconnecting", "Cannot use /toggle while Mare Synchronos is still disconnecting",
NotificationType.Error));
}
if (_serverConfigurationManager.CurrentServer == null) return;
var fullPause = splitArgs.Length > 1 ? splitArgs[1] switch
{
"on" => false,
"off" => true,
_ => !_serverConfigurationManager.CurrentServer.FullPause,
} : !_serverConfigurationManager.CurrentServer.FullPause;
if (fullPause != _serverConfigurationManager.CurrentServer.FullPause)
{
_serverConfigurationManager.CurrentServer.FullPause = fullPause;
_serverConfigurationManager.Save();
_ = _apiController.CreateConnectionsAsync();
}
}
else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase))
{
_mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
}
else if (string.Equals(splitArgs[0], "rescan", StringComparison.OrdinalIgnoreCase))
{
_cacheMonitor.InvokeScan();
}
else if (string.Equals(splitArgs[0], "perf", StringComparison.OrdinalIgnoreCase))
{
if (splitArgs.Length > 1 && int.TryParse(splitArgs[1], CultureInfo.InvariantCulture, out var limitBySeconds))
{
_performanceCollectorService.PrintPerformanceStats(limitBySeconds);
}
else
{
_performanceCollectorService.PrintPerformanceStats();
}
}
else if (string.Equals(splitArgs[0], "medi", StringComparison.OrdinalIgnoreCase))
{
_mediator.PrintSubscriberInfo();
}
else if (string.Equals(splitArgs[0], "analyze", StringComparison.OrdinalIgnoreCase))
{
_mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
}
else if (string.Equals(splitArgs[0], "settings", StringComparison.OrdinalIgnoreCase))
{
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
}
}
}

View File

@@ -0,0 +1,776 @@
using Dalamud.Game;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.Sheets;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.Interop;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
namespace MareSynchronos.Services;
public class DalamudUtilService : IHostedService, IMediatorSubscriber
{
private readonly List<uint> _classJobIdsIgnoredForPets = [30];
private readonly IClientState _clientState;
private readonly ICondition _condition;
private readonly IDataManager _gameData;
private readonly IGameConfig _gameConfig;
private readonly BlockedCharacterHandler _blockedCharacterHandler;
private readonly IFramework _framework;
private readonly IGameGui _gameGui;
private readonly ILogger<DalamudUtilService> _logger;
private readonly IObjectTable _objectTable;
private readonly PerformanceCollectorService _performanceCollector;
private readonly MareConfigService _configService;
private uint? _classJobId = 0;
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
private string _lastGlobalBlockPlayer = string.Empty;
private string _lastGlobalBlockReason = string.Empty;
private ushort _lastZone = 0;
private readonly Dictionary<string, (string Name, nint Address)> _playerCharas = new(StringComparer.Ordinal);
private readonly List<string> _notUpdatedCharas = [];
private bool _sentBetweenAreas = false;
private Lazy<ulong> _cid;
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig,
BlockedCharacterHandler blockedCharacterHandler, MareMediator mediator, PerformanceCollectorService performanceCollector,
MareConfigService configService)
{
_logger = logger;
_clientState = clientState;
_objectTable = objectTable;
_framework = framework;
_gameGui = gameGui;
_condition = condition;
_gameData = gameData;
_gameConfig = gameConfig;
_blockedCharacterHandler = blockedCharacterHandler;
Mediator = mediator;
_performanceCollector = performanceCollector;
_configService = configService;
WorldData = new(() =>
{
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(Dalamud.Game.ClientLanguage.English)!
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])))
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
});
JobData = new(() =>
{
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
});
TerritoryData = new(() =>
{
return gameData.GetExcelSheet<TerritoryType>(Dalamud.Game.ClientLanguage.English)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
StringBuilder sb = new();
sb.Append(w.PlaceNameRegion.Value.Name);
if (w.PlaceName.ValueNullable != null)
{
sb.Append(" - ");
sb.Append(w.PlaceName.Value.Name);
}
return sb.ToString();
});
});
MapData = new(() =>
{
return gameData.GetExcelSheet<Map>(Dalamud.Game.ClientLanguage.English)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
StringBuilder sb = new();
sb.Append(w.PlaceNameRegion.Value.Name);
if (w.PlaceName.ValueNullable != null)
{
sb.Append(" - ");
sb.Append(w.PlaceName.Value.Name);
}
if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString()))
{
sb.Append(" - ");
sb.Append(w.PlaceNameSub.Value.Name);
}
return (w, sb.ToString());
});
});
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
{
if (clientState.IsPvP) return;
var name = msg.Pair.PlayerName;
if (string.IsNullOrEmpty(name)) return;
var addr = _playerCharas.FirstOrDefault(f => string.Equals(f.Value.Name, name, StringComparison.Ordinal)).Value.Address;
if (addr == nint.Zero) return;
var useFocusTarget = _configService.Current.UseFocusTarget;
_ = RunOnFrameworkThread(() =>
{
if (useFocusTarget)
targetManager.FocusTarget = CreateGameObject(addr);
else
targetManager.Target = CreateGameObject(addr);
}).ConfigureAwait(false);
});
IsWine = Util.IsWine();
_cid = RebuildCID();
}
private Lazy<ulong> RebuildCID() => new(GetCID);
public bool IsWine { get; init; }
public unsafe GameObject* GposeTarget
{
get => TargetSystem.Instance()->GPoseTarget;
set => TargetSystem.Instance()->GPoseTarget = value;
}
private unsafe bool HasGposeTarget => GposeTarget != null;
private unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex;
public async Task<IGameObject?> GetGposeTargetGameObjectAsync()
{
if (!HasGposeTarget)
return null;
return await _framework.RunOnFrameworkThread(() => _objectTable[GPoseTargetIdx]).ConfigureAwait(true);
}
public bool IsAnythingDrawing { get; private set; } = false;
public bool IsInCutscene { get; private set; } = false;
public bool IsInGpose { get; private set; } = false;
public bool IsLoggedIn { get; private set; }
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
public bool IsInCombatOrPerforming { get; private set; } = false;
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
public uint ClassJobId => _classJobId!.Value;
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
public bool IsLodEnabled { get; private set; }
public MareMediator Mediator { get; }
public IGameObject? CreateGameObject(IntPtr reference)
{
EnsureIsOnFramework();
return _objectTable.CreateObjectReference(reference);
}
public async Task<IGameObject?> CreateGameObjectAsync(IntPtr reference)
{
return await RunOnFrameworkThread(() => _objectTable.CreateObjectReference(reference)).ConfigureAwait(false);
}
public void EnsureIsOnFramework()
{
if (!_framework.IsInFrameworkUpdateThread) throw new InvalidOperationException("Can only be run on Framework");
}
public ICharacter? GetCharacterFromObjectTableByIndex(int index)
{
EnsureIsOnFramework();
var objTableObj = _objectTable[index];
if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null;
return (ICharacter)objTableObj;
}
public unsafe IntPtr GetCompanionPtr(IntPtr? playerPointer = null)
{
EnsureIsOnFramework();
var mgr = CharacterManager.Instance();
playerPointer ??= GetPlayerPtr();
if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero;
return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer);
}
public async Task<IntPtr> GetCompanionAsync(IntPtr? playerPointer = null)
{
return await RunOnFrameworkThread(() => GetCompanionPtr(playerPointer)).ConfigureAwait(false);
}
public async Task<ICharacter?> GetGposeCharacterFromObjectTableByNameAsync(string name, bool onlyGposeCharacters = false)
{
return await RunOnFrameworkThread(() => GetGposeCharacterFromObjectTableByName(name, onlyGposeCharacters)).ConfigureAwait(false);
}
public ICharacter? GetGposeCharacterFromObjectTableByName(string name, bool onlyGposeCharacters = false)
{
EnsureIsOnFramework();
return (ICharacter?)_objectTable
.FirstOrDefault(i => (!onlyGposeCharacters || i.ObjectIndex >= 200) && string.Equals(i.Name.ToString(), name, StringComparison.Ordinal));
}
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
{
return _objectTable.Where(o => o.ObjectIndex > 200 && o.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player).Cast<ICharacter>();
}
public bool GetIsPlayerPresent()
{
EnsureIsOnFramework();
return _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid();
}
public async Task<bool> GetIsPlayerPresentAsync()
{
return await RunOnFrameworkThread(GetIsPlayerPresent).ConfigureAwait(false);
}
public unsafe IntPtr GetMinionOrMountPtr(IntPtr? playerPointer = null)
{
EnsureIsOnFramework();
playerPointer ??= GetPlayerPtr();
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1);
}
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
{
return await RunOnFrameworkThread(() => GetMinionOrMountPtr(playerPointer)).ConfigureAwait(false);
}
public unsafe IntPtr GetPetPtr(IntPtr? playerPointer = null)
{
EnsureIsOnFramework();
if (_classJobIdsIgnoredForPets.Contains(_classJobId ?? 0)) return IntPtr.Zero;
var mgr = CharacterManager.Instance();
playerPointer ??= GetPlayerPtr();
if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero;
return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer);
}
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
{
return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false);
}
public async Task<IPlayerCharacter> GetPlayerCharacterAsync()
{
return await RunOnFrameworkThread(GetPlayerCharacter).ConfigureAwait(false);
}
public IPlayerCharacter GetPlayerCharacter()
{
EnsureIsOnFramework();
return _clientState.LocalPlayer!;
}
public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName)
{
if (_playerCharas.TryGetValue(characterName, out var pchar)) return pchar.Address;
return IntPtr.Zero;
}
public string GetPlayerName()
{
EnsureIsOnFramework();
return _clientState.LocalPlayer?.Name.ToString() ?? "--";
}
public async Task<string> GetPlayerNameAsync()
{
return await RunOnFrameworkThread(GetPlayerName).ConfigureAwait(false);
}
public async Task<ulong> GetCIDAsync()
{
return await RunOnFrameworkThread(GetCID).ConfigureAwait(false);
}
public unsafe ulong GetCID()
{
EnsureIsOnFramework();
var playerChar = GetPlayerCharacter();
return ((BattleChara*)playerChar.Address)->Character.ContentId;
}
public async Task<string> GetPlayerNameHashedAsync()
{
return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false);
}
private unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
{
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
}
public IntPtr GetPlayerPtr()
{
EnsureIsOnFramework();
return _clientState.LocalPlayer?.Address ?? IntPtr.Zero;
}
public async Task<IntPtr> GetPlayerPointerAsync()
{
return await RunOnFrameworkThread(GetPlayerPtr).ConfigureAwait(false);
}
public uint GetHomeWorldId()
{
EnsureIsOnFramework();
return _clientState.LocalPlayer?.HomeWorld.RowId ?? 0;
}
public uint GetWorldId()
{
EnsureIsOnFramework();
return _clientState.LocalPlayer!.CurrentWorld.RowId;
}
public unsafe LocationInfo GetMapData()
{
EnsureIsOnFramework();
var agentMap = AgentMap.Instance();
var houseMan = HousingManager.Instance();
uint serverId = 0;
if (_clientState.LocalPlayer == null) serverId = 0;
else serverId = _clientState.LocalPlayer.CurrentWorld.RowId;
uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId;
uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId;
uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision());
uint wardId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentWard() + 1);
uint houseId = 0;
var tempHouseId = houseMan == null ? 0 : (houseMan->GetCurrentPlot());
if (!houseMan->IsInside()) tempHouseId = 0;
if (tempHouseId < -1)
{
divisionId = tempHouseId == -127 ? 2 : (uint)1;
tempHouseId = 100;
}
if (tempHouseId == -1) tempHouseId = 0;
houseId = (uint)tempHouseId;
if (houseId != 0)
{
territoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
}
uint roomId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentRoom());
return new LocationInfo()
{
ServerId = serverId,
MapId = mapId,
TerritoryId = territoryId,
DivisionId = divisionId,
WardId = wardId,
HouseId = houseId,
RoomId = roomId
};
}
public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
{
EnsureIsOnFramework();
var agentMap = AgentMap.Instance();
if (agentMap == null) return;
agentMap->OpenMapByMapId(map.RowId);
agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position);
}
public async Task<LocationInfo> GetMapDataAsync()
{
return await RunOnFrameworkThread(GetMapData).ConfigureAwait(false);
}
public async Task<uint> GetWorldIdAsync()
{
return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false);
}
public async Task<uint> GetHomeWorldIdAsync()
{
return await RunOnFrameworkThread(GetHomeWorldId).ConfigureAwait(false);
}
public unsafe bool IsGameObjectPresent(IntPtr key)
{
return _objectTable.Any(f => f.Address == key);
}
public bool IsObjectPresent(IGameObject? obj)
{
EnsureIsOnFramework();
return obj != null && obj.IsValid();
}
public async Task<bool> IsObjectPresentAsync(IGameObject? obj)
{
return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false);
}
public async Task RunOnFrameworkThread(System.Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
{
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
await _performanceCollector.LogPerformance(this, $"RunOnFramework:Act/{fileName}>{callerMember}:{callerLineNumber}", async () =>
{
if (!_framework.IsInFrameworkUpdateThread)
{
await _framework.RunOnFrameworkThread(act).ContinueWith((_) => Task.CompletedTask).ConfigureAwait(false);
while (_framework.IsInFrameworkUpdateThread) // yield the thread again, should technically never be triggered
{
_logger.LogTrace("Still on framework");
await Task.Delay(1).ConfigureAwait(false);
}
}
else
act();
}).ConfigureAwait(false);
}
public async Task<T> RunOnFrameworkThread<T>(Func<T> func, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
{
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
return await _performanceCollector.LogPerformance(this, $"RunOnFramework:Func<{typeof(T)}>/{fileName}>{callerMember}:{callerLineNumber}", async () =>
{
if (!_framework.IsInFrameworkUpdateThread)
{
var result = await _framework.RunOnFrameworkThread(func).ContinueWith((task) => task.Result).ConfigureAwait(false);
while (_framework.IsInFrameworkUpdateThread) // yield the thread again, should technically never be triggered
{
_logger.LogTrace("Still on framework");
await Task.Delay(1).ConfigureAwait(false);
}
return result;
}
return func.Invoke();
}).ConfigureAwait(false);
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting DalamudUtilService");
_framework.Update += FrameworkOnUpdate;
if (IsLoggedIn)
{
_classJobId = _clientState.LocalPlayer!.ClassJob.RowId;
}
_logger.LogInformation("Started DalamudUtilService");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogTrace("Stopping {type}", GetType());
Mediator.UnsubscribeAll(this);
_framework.Update -= FrameworkOnUpdate;
return Task.CompletedTask;
}
public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
{
if (!_clientState.IsLoggedIn) return;
if (ct == null)
ct = CancellationToken.None;
const int tick = 250;
int curWaitTime = 0;
try
{
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
curWaitTime += tick;
while ((!ct.Value.IsCancellationRequested)
&& curWaitTime < timeOut
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something
{
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
curWaitTime += tick;
await Task.Delay(tick).ConfigureAwait(true);
}
logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
}
catch (NullReferenceException ex)
{
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
}
catch (AccessViolationException ex)
{
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
}
}
public unsafe void WaitWhileGposeCharacterIsDrawing(IntPtr characterAddress, int timeOut = 5000)
{
Thread.Sleep(500);
var obj = (GameObject*)characterAddress;
const int tick = 250;
int curWaitTime = 0;
_logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X"));
while (obj->RenderFlags != 0x00 && curWaitTime < timeOut)
{
_logger.LogTrace($"Waiting for gpose actor to finish drawing");
curWaitTime += tick;
Thread.Sleep(tick);
}
Thread.Sleep(tick * 2);
}
public Vector2 WorldToScreen(IGameObject? obj)
{
if (obj == null) return Vector2.Zero;
return _gameGui.WorldToScreen(obj.Position, out var screenPos) ? screenPos : Vector2.Zero;
}
internal (string Name, nint Address) FindPlayerByNameHash(string ident)
{
_playerCharas.TryGetValue(ident, out var result);
return result;
}
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
{
var gameObj = (GameObject*)address;
var drawObj = gameObj->DrawObject;
bool isDrawing = false;
bool isDrawingChanged = false;
if ((nint)drawObj != IntPtr.Zero)
{
isDrawing = gameObj->RenderFlags == 0b100000000000;
if (!isDrawing)
{
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
if (!isDrawing)
{
isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0;
if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
{
_lastGlobalBlockPlayer = characterName;
_lastGlobalBlockReason = "HasModelFilesInSlotLoaded";
isDrawingChanged = true;
}
}
else
{
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
&& !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal))
{
_lastGlobalBlockPlayer = characterName;
_lastGlobalBlockReason = "HasModelInSlotLoaded";
isDrawingChanged = true;
}
}
}
else
{
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
&& !string.Equals(_lastGlobalBlockReason, "RenderFlags", StringComparison.Ordinal))
{
_lastGlobalBlockPlayer = characterName;
_lastGlobalBlockReason = "RenderFlags";
isDrawingChanged = true;
}
}
}
if (isDrawingChanged)
{
_logger.LogTrace("Global draw block: START => {name} ({reason})", characterName, _lastGlobalBlockReason);
}
IsAnythingDrawing |= isDrawing;
}
private void FrameworkOnUpdate(IFramework framework)
{
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdate", FrameworkOnUpdateInternal);
}
private unsafe void FrameworkOnUpdateInternal()
{
if ((_clientState.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
{
return;
}
bool isNormalFrameworkUpdate = DateTime.UtcNow < _delayedFrameworkUpdateCheck.AddSeconds(1);
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () =>
{
IsAnythingDrawing = false;
_performanceCollector.LogPerformance(this, $"ObjTableToCharas",
() =>
{
_notUpdatedCharas.AddRange(_playerCharas.Keys);
for (int i = 0; i < 200; i += 2)
{
var chara = _objectTable[i];
if (chara == null || chara.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player)
continue;
if (_blockedCharacterHandler.IsCharacterBlocked(chara.Address, out bool firstTime) && firstTime)
{
_logger.LogTrace("Skipping character {addr}, blocked/muted", chara.Address.ToString("X"));
continue;
}
var charaName = ((GameObject*)chara.Address)->NameString;
var hash = GetHashedCIDFromPlayerPointer(chara.Address);
if (!IsAnythingDrawing)
CheckCharacterForDrawing(chara.Address, charaName);
_notUpdatedCharas.Remove(hash);
_playerCharas[hash] = (charaName, chara.Address);
}
foreach (var notUpdatedChara in _notUpdatedCharas)
{
_playerCharas.Remove(notUpdatedChara);
}
_notUpdatedCharas.Clear();
});
if (!IsAnythingDrawing && !string.IsNullOrEmpty(_lastGlobalBlockPlayer))
{
_logger.LogTrace("Global draw block: END => {name}", _lastGlobalBlockPlayer);
_lastGlobalBlockPlayer = string.Empty;
_lastGlobalBlockReason = string.Empty;
}
if (_clientState.IsGPosing && !IsInGpose)
{
_logger.LogDebug("Gpose start");
IsInGpose = true;
Mediator.Publish(new GposeStartMessage());
}
else if (!_clientState.IsGPosing && IsInGpose)
{
_logger.LogDebug("Gpose end");
IsInGpose = false;
Mediator.Publish(new GposeEndMessage());
}
if ((_condition[ConditionFlag.Performing] || _condition[ConditionFlag.InCombat]) && !IsInCombatOrPerforming)
{
_logger.LogDebug("Combat/Performance start");
IsInCombatOrPerforming = true;
Mediator.Publish(new CombatOrPerformanceStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInCombatOrPerforming)));
}
else if ((!_condition[ConditionFlag.Performing] && !_condition[ConditionFlag.InCombat]) && IsInCombatOrPerforming)
{
_logger.LogDebug("Combat/Performance end");
IsInCombatOrPerforming = false;
Mediator.Publish(new CombatOrPerformanceEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsInCombatOrPerforming)));
}
if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene)
{
_logger.LogDebug("Cutscene start");
IsInCutscene = true;
Mediator.Publish(new CutsceneStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene)));
}
else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene)
{
_logger.LogDebug("Cutscene end");
IsInCutscene = false;
Mediator.Publish(new CutsceneEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene)));
}
if (IsInCutscene)
{
Mediator.Publish(new CutsceneFrameworkUpdateMessage());
return;
}
if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51])
{
var zone = _clientState.TerritoryType;
if (_lastZone != zone)
{
_lastZone = zone;
if (!_sentBetweenAreas)
{
_logger.LogDebug("Zone switch start");
_sentBetweenAreas = true;
Mediator.Publish(new ZoneSwitchStartMessage());
Mediator.Publish(new HaltScanMessage(nameof(ConditionFlag.BetweenAreas)));
}
}
return;
}
if (_sentBetweenAreas)
{
_logger.LogDebug("Zone switch end");
_sentBetweenAreas = false;
Mediator.Publish(new ZoneSwitchEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
}
var localPlayer = _clientState.LocalPlayer;
if (localPlayer != null)
{
_classJobId = localPlayer.ClassJob.RowId;
}
if (!IsInCombatOrPerforming)
Mediator.Publish(new FrameworkUpdateMessage());
Mediator.Publish(new PriorityFrameworkUpdateMessage());
if (isNormalFrameworkUpdate)
return;
if (localPlayer != null && !IsLoggedIn)
{
_logger.LogDebug("Logged in");
IsLoggedIn = true;
_lastZone = _clientState.TerritoryType;
_cid = RebuildCID();
Mediator.Publish(new DalamudLoginMessage());
}
else if (localPlayer == null && IsLoggedIn)
{
_logger.LogDebug("Logged out");
IsLoggedIn = false;
Mediator.Publish(new DalamudLogoutMessage());
}
if (_gameConfig != null
&& _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
{
IsLodEnabled = lodEnabled;
}
if (IsInCombatOrPerforming)
Mediator.Publish(new FrameworkUpdateMessage());
Mediator.Publish(new DelayedFrameworkUpdateMessage());
_delayedFrameworkUpdateCheck = DateTime.UtcNow;
});
}
}

View File

@@ -0,0 +1,45 @@
using MareSynchronos.API.Data;
namespace MareSynchronos.Services.Events;
public record Event
{
public DateTime EventTime { get; }
public string UID { get; }
public string Character { get; }
public string EventSource { get; }
public EventSeverity EventSeverity { get; }
public string Message { get; }
public Event(string? Character, UserData UserData, string EventSource, EventSeverity EventSeverity, string Message)
{
EventTime = DateTime.Now;
this.UID = UserData.AliasOrUID;
this.Character = Character ?? string.Empty;
this.EventSource = EventSource;
this.EventSeverity = EventSeverity;
this.Message = Message;
}
public Event(UserData UserData, string EventSource, EventSeverity EventSeverity, string Message) : this(null, UserData, EventSource, EventSeverity, Message)
{
}
public Event(string EventSource, EventSeverity EventSeverity, string Message)
: this(new UserData(string.Empty), EventSource, EventSeverity, Message)
{
}
public override string ToString()
{
if (string.IsNullOrEmpty(UID))
return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t{Message}";
else
{
if (string.IsNullOrEmpty(Character))
return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}> {Message}";
else
return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}\\{Character}> {Message}";
}
}
}

View File

@@ -0,0 +1,111 @@
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.Events;
public class EventAggregator : MediatorSubscriberBase, IHostedService
{
private readonly RollingList<Event> _events = new(500);
private readonly SemaphoreSlim _lock = new(1);
private readonly string _configDirectory;
private readonly ILogger<EventAggregator> _logger;
public Lazy<List<Event>> EventList { get; private set; }
public bool NewEventsAvailable => !EventList.IsValueCreated;
public string EventLogFolder => Path.Combine(_configDirectory, "eventlog");
private string CurrentLogName => $"{DateTime.Now:yyyy-MM-dd}-events.log";
private DateTime _currentTime;
public EventAggregator(string configDirectory, ILogger<EventAggregator> logger, MareMediator mareMediator) : base(logger, mareMediator)
{
Mediator.Subscribe<EventMessage>(this, (msg) =>
{
_lock.Wait();
try
{
Logger.LogTrace("Received Event: {evt}", msg.Event.ToString());
_events.Add(msg.Event);
WriteToFile(msg.Event);
}
finally
{
_lock.Release();
}
RecreateLazy();
});
EventList = CreateEventLazy();
_configDirectory = configDirectory;
_logger = logger;
_currentTime = DateTime.Now - TimeSpan.FromDays(1);
}
private void RecreateLazy()
{
if (!EventList.IsValueCreated) return;
EventList = CreateEventLazy();
}
private Lazy<List<Event>> CreateEventLazy()
{
return new Lazy<List<Event>>(() =>
{
_lock.Wait();
try
{
return [.. _events];
}
finally
{
_lock.Release();
}
});
}
private void WriteToFile(Event receivedEvent)
{
if (DateTime.Now.Day != _currentTime.Day)
{
try
{
_currentTime = DateTime.Now;
var filesInDirectory = Directory.EnumerateFiles(EventLogFolder, "*.log");
if (filesInDirectory.Skip(10).Any())
{
File.Delete(filesInDirectory.OrderBy(f => new FileInfo(f).LastWriteTimeUtc).First());
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not delete last events");
}
}
var eventLogFile = Path.Combine(EventLogFolder, CurrentLogName);
try
{
if (!Directory.Exists(EventLogFolder)) Directory.CreateDirectory(EventLogFolder);
File.AppendAllLines(eventLogFile, [receivedEvent.ToString()]);
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Could not write to event file {eventLogFile}");
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
Logger.LogInformation("Starting EventAggregatorService");
Logger.LogInformation("Started EventAggregatorService");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronos.Services.Events;
public enum EventSeverity
{
Informational = 0,
Warning = 1,
Error = 2
}

View File

@@ -0,0 +1,7 @@
namespace MareSynchronos.Services;
public record MareProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description)
{
public Lazy<byte[]> ImageData { get; } = new Lazy<byte[]>(Convert.FromBase64String(Base64ProfilePicture));
public Lazy<byte[]> SupporterImageData { get; } = new Lazy<byte[]>(string.IsNullOrEmpty(Base64SupporterPicture) ? [] : Convert.FromBase64String(Base64SupporterPicture));
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.Mediator;
public abstract class DisposableMediatorSubscriberBase : MediatorSubscriberBase, IDisposable
{
protected DisposableMediatorSubscriberBase(ILogger logger, MareMediator mediator) : base(logger, mediator)
{
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
Logger.LogTrace("Disposing {type} ({this})", GetType().Name, this);
UnsubscribeAll();
}
}

View File

@@ -0,0 +1,3 @@
namespace MareSynchronos.Services.Mediator;
public interface IHighPriorityMediatorSubscriber : IMediatorSubscriber { }

View File

@@ -0,0 +1,6 @@
namespace MareSynchronos.Services.Mediator;
public interface IMediatorSubscriber
{
MareMediator Mediator { get; }
}

View File

@@ -0,0 +1,207 @@
using MareSynchronos.MareConfiguration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Reflection;
using System.Text;
namespace MareSynchronos.Services.Mediator;
public sealed class MareMediator : IHostedService
{
private readonly object _addRemoveLock = new();
private readonly ConcurrentDictionary<object, DateTime> _lastErrorTime = [];
private readonly ILogger<MareMediator> _logger;
private readonly CancellationTokenSource _loopCts = new();
private readonly ConcurrentQueue<MessageBase> _messageQueue = new();
private readonly PerformanceCollectorService _performanceCollector;
private readonly MareConfigService _mareConfigService;
private readonly ConcurrentDictionary<Type, HashSet<SubscriberAction>> _subscriberDict = [];
private bool _processQueue = false;
private readonly ConcurrentDictionary<Type, MethodInfo?> _genericExecuteMethods = new();
public MareMediator(ILogger<MareMediator> logger, PerformanceCollectorService performanceCollector, MareConfigService mareConfigService)
{
_logger = logger;
_performanceCollector = performanceCollector;
_mareConfigService = mareConfigService;
}
public void PrintSubscriberInfo()
{
foreach (var subscriber in _subscriberDict.SelectMany(c => c.Value.Select(v => v.Subscriber))
.DistinctBy(p => p).OrderBy(p => p.GetType().FullName, StringComparer.Ordinal).ToList())
{
_logger.LogInformation("Subscriber {type}: {sub}", subscriber.GetType().Name, subscriber.ToString());
StringBuilder sb = new();
sb.Append("=> ");
foreach (var item in _subscriberDict.Where(item => item.Value.Any(v => v.Subscriber == subscriber)).ToList())
{
sb.Append(item.Key.Name).Append(", ");
}
if (!string.Equals(sb.ToString(), "=> ", StringComparison.Ordinal))
_logger.LogInformation("{sb}", sb.ToString());
_logger.LogInformation("---");
}
}
public void Publish<T>(T message) where T : MessageBase
{
if (message.KeepThreadContext)
{
ExecuteMessage(message);
}
else
{
_messageQueue.Enqueue(message);
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting MareMediator");
_ = Task.Run(async () =>
{
while (!_loopCts.Token.IsCancellationRequested)
{
while (!_processQueue)
{
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
}
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
HashSet<MessageBase> processedMessages = [];
while (_messageQueue.TryDequeue(out var message))
{
if (processedMessages.Contains(message)) { continue; }
processedMessages.Add(message);
ExecuteMessage(message);
}
}
});
_logger.LogInformation("Started MareMediator");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_messageQueue.Clear();
_loopCts.Cancel();
_loopCts.Dispose();
return Task.CompletedTask;
}
public void Subscribe<T>(IMediatorSubscriber subscriber, Action<T> action) where T : MessageBase
{
lock (_addRemoveLock)
{
_subscriberDict.TryAdd(typeof(T), []);
if (!_subscriberDict[typeof(T)].Add(new(subscriber, action)))
{
throw new InvalidOperationException("Already subscribed");
}
}
}
public void Unsubscribe<T>(IMediatorSubscriber subscriber) where T : MessageBase
{
lock (_addRemoveLock)
{
if (_subscriberDict.ContainsKey(typeof(T)))
{
_subscriberDict[typeof(T)].RemoveWhere(p => p.Subscriber == subscriber);
}
}
}
internal void UnsubscribeAll(IMediatorSubscriber subscriber)
{
lock (_addRemoveLock)
{
foreach (Type kvp in _subscriberDict.Select(k => k.Key))
{
int unSubbed = _subscriberDict[kvp]?.RemoveWhere(p => p.Subscriber == subscriber) ?? 0;
if (unSubbed > 0)
{
_logger.LogDebug("{sub} unsubscribed from {msg}", subscriber.GetType().Name, kvp.Name);
}
}
}
}
private void ExecuteMessage(MessageBase message)
{
if (!_subscriberDict.TryGetValue(message.GetType(), out HashSet<SubscriberAction>? subscribers) || subscribers == null || !subscribers.Any()) return;
List<SubscriberAction> subscribersCopy = [];
lock (_addRemoveLock)
{
subscribersCopy = subscribers?.Where(s => s.Subscriber != null).OrderBy(k => k.Subscriber is IHighPriorityMediatorSubscriber ? 0 : 1).ToList() ?? [];
}
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
var msgType = message.GetType();
if (!_genericExecuteMethods.TryGetValue(msgType, out var methodInfo))
{
_genericExecuteMethods[msgType] = methodInfo = GetType()
.GetMethod(nameof(ExecuteReflected), BindingFlags.NonPublic | BindingFlags.Instance)?
.MakeGenericMethod(msgType);
}
methodInfo!.Invoke(this, [subscribersCopy, message]);
#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
}
private void ExecuteReflected<T>(List<SubscriberAction> subscribers, T message) where T : MessageBase
{
foreach (SubscriberAction subscriber in subscribers)
{
try
{
if (_mareConfigService.Current.LogPerformance)
{
var isSameThread = message.KeepThreadContext ? "$" : string.Empty;
_performanceCollector.LogPerformance(this, $"{isSameThread}Execute>{message.GetType().Name}+{subscriber.Subscriber.GetType().Name}>{subscriber.Subscriber}",
() => ((Action<T>)subscriber.Action).Invoke(message));
}
else
{
((Action<T>)subscriber.Action).Invoke(message);
}
}
catch (Exception ex)
{
if (_lastErrorTime.TryGetValue(subscriber, out var lastErrorTime) && lastErrorTime.Add(TimeSpan.FromSeconds(10)) > DateTime.UtcNow)
continue;
_logger.LogError(ex.InnerException ?? ex, "Error executing {type} for subscriber {subscriber}",
message.GetType().Name, subscriber.Subscriber.GetType().Name);
_lastErrorTime[subscriber] = DateTime.UtcNow;
}
}
}
public void StartQueueProcessing()
{
_logger.LogInformation("Starting Message Queue Processing");
_processQueue = true;
}
private sealed class SubscriberAction
{
public SubscriberAction(IMediatorSubscriber subscriber, object action)
{
Subscriber = subscriber;
Action = action;
}
public object Action { get; }
public IMediatorSubscriber Subscriber { get; }
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.Mediator;
public abstract class MediatorSubscriberBase : IMediatorSubscriber
{
protected MediatorSubscriberBase(ILogger logger, MareMediator mediator)
{
Logger = logger;
Logger.LogTrace("Creating {type} ({this})", GetType().Name, this);
Mediator = mediator;
}
public MareMediator Mediator { get; }
protected ILogger Logger { get; }
protected void UnsubscribeAll()
{
Logger.LogTrace("Unsubscribing from all for {type} ({this})", GetType().Name, this);
Mediator.UnsubscribeAll(this);
}
}

View File

@@ -0,0 +1,13 @@
namespace MareSynchronos.Services.Mediator;
#pragma warning disable MA0048
public abstract record MessageBase
{
public virtual bool KeepThreadContext => false;
}
public record SameThreadMessage : MessageBase
{
public override bool KeepThreadContext => true;
}
#pragma warning restore MA0048

View File

@@ -0,0 +1,97 @@
using Dalamud.Game.ClientState.Objects.Types;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Events;
using MareSynchronos.WebAPI.Files.Models;
using System.Numerics;
namespace MareSynchronos.Services.Mediator;
#pragma warning disable MA0048 // File name must match type name
#pragma warning disable S2094
public record SwitchToIntroUiMessage : MessageBase;
public record SwitchToMainUiMessage : MessageBase;
public record OpenSettingsUiMessage : MessageBase;
public record DalamudLoginMessage : MessageBase;
public record DalamudLogoutMessage : MessageBase;
public record PriorityFrameworkUpdateMessage : SameThreadMessage;
public record FrameworkUpdateMessage : SameThreadMessage;
public record ClassJobChangedMessage(GameObjectHandler GameObjectHandler) : MessageBase;
public record DelayedFrameworkUpdateMessage : SameThreadMessage;
public record ZoneSwitchStartMessage : MessageBase;
public record ZoneSwitchEndMessage : MessageBase;
public record CutsceneStartMessage : MessageBase;
public record GposeStartMessage : SameThreadMessage;
public record GposeEndMessage : MessageBase;
public record CutsceneEndMessage : MessageBase;
public record CutsceneFrameworkUpdateMessage : SameThreadMessage;
public record ConnectedMessage(ConnectionDto Connection) : MessageBase;
public record DisconnectedMessage : SameThreadMessage;
public record PenumbraModSettingChangedMessage : MessageBase;
public record PenumbraInitializedMessage : MessageBase;
public record PenumbraDisposedMessage : MessageBase;
public record PenumbraRedrawMessage(IntPtr Address, int ObjTblIdx, bool WasRequested) : SameThreadMessage;
public record GlamourerChangedMessage(IntPtr Address) : MessageBase;
public record HeelsOffsetMessage : MessageBase;
public record PenumbraResourceLoadMessage(IntPtr GameObject, string GamePath, string FilePath) : SameThreadMessage;
public record CustomizePlusMessage(nint? Address) : MessageBase;
public record HonorificMessage(string NewHonorificTitle) : MessageBase;
public record MoodlesMessage(IntPtr Address) : MessageBase;
public record PetNamesReadyMessage : MessageBase;
public record PetNamesMessage(string PetNicknamesData) : MessageBase;
public record HonorificReadyMessage : MessageBase;
public record TransientResourceChangedMessage(IntPtr Address) : MessageBase;
public record HaltScanMessage(string Source) : MessageBase;
public record ResumeScanMessage(string Source) : MessageBase;
public record NotificationMessage
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
public record CharacterDataAnalyzedMessage : MessageBase;
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage;
public record HubReconnectedMessage(string? Arg) : SameThreadMessage;
public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
public record UiToggleMessage(Type UiType) : MessageBase;
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
public record ClearProfileDataMessage(UserData? UserData = null) : MessageBase;
public record CyclePauseMessage(UserData UserData) : MessageBase;
public record PauseMessage(UserData UserData) : MessageBase;
public record ProfilePopoutToggle(Pair? Pair) : MessageBase;
public record CompactUiChange(Vector2 Size, Vector2 Position) : MessageBase;
public record ProfileOpenStandaloneMessage(Pair Pair) : MessageBase;
public record RemoveWindowMessage(WindowMediatorSubscriberBase Window) : MessageBase;
public record RefreshUiMessage : MessageBase;
public record OpenBanUserPopupMessage(Pair PairToBan, GroupFullInfoDto GroupFullInfoDto) : MessageBase;
public record OpenCensusPopupMessage() : MessageBase;
public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase;
public record OpenPermissionWindow(Pair Pair) : MessageBase;
public record DownloadLimitChangedMessage() : SameThreadMessage;
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
public record TargetPairMessage(Pair Pair) : MessageBase;
public record CombatOrPerformanceStartMessage : MessageBase;
public record CombatOrPerformanceEndMessage : MessageBase;
public record EventMessage(Event Event) : MessageBase;
public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBase;
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
public record GameObjectHandlerCreatedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : SameThreadMessage;
public record GameObjectHandlerDestroyedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : SameThreadMessage;
public record HaltCharaDataCreation(bool Resume = false) : SameThreadMessage;
public record GposeLobbyUserJoin(UserData UserData) : MessageBase;
public record GPoseLobbyUserLeave(UserData UserData) : MessageBase;
public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadDto) : MessageBase;
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
#pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name

View File

@@ -0,0 +1,54 @@
using Dalamud.Interface.Windowing;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.Mediator;
public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber, IDisposable
{
protected readonly ILogger _logger;
private readonly PerformanceCollectorService _performanceCollectorService;
protected WindowMediatorSubscriberBase(ILogger logger, MareMediator mediator, string name,
PerformanceCollectorService performanceCollectorService) : base(name)
{
_logger = logger;
Mediator = mediator;
_performanceCollectorService = performanceCollectorService;
_logger.LogTrace("Creating {type}", GetType());
Mediator.Subscribe<UiToggleMessage>(this, (msg) =>
{
if (msg.UiType == GetType())
{
Toggle();
}
});
}
public MareMediator Mediator { get; }
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public override void Draw()
{
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);
}
protected abstract void DrawInternal();
public virtual Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected virtual void Dispose(bool disposing)
{
_logger.LogTrace("Disposing {type}", GetType());
Mediator.UnsubscribeAll(this);
}
}

View File

@@ -0,0 +1,141 @@
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
namespace MareSynchronos.Services;
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
{
private readonly DalamudUtilService _dalamudUtilService;
private readonly INotificationManager _notificationManager;
private readonly IChatGui _chatGui;
private readonly MareConfigService _configurationService;
public NotificationService(ILogger<NotificationService> logger, MareMediator mediator,
DalamudUtilService dalamudUtilService,
INotificationManager notificationManager,
IChatGui chatGui, MareConfigService configurationService) : base(logger, mediator)
{
_dalamudUtilService = dalamudUtilService;
_notificationManager = notificationManager;
_chatGui = chatGui;
_configurationService = configurationService;
}
public Task StartAsync(CancellationToken cancellationToken)
{
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void PrintErrorChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Error: " + message);
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Info: ").AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void PrintWarnChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
_chatGui.Print(se.BuiltString);
}
private void ShowChat(NotificationMessage msg)
{
switch (msg.Type)
{
case NotificationType.Info:
PrintInfoChat(msg.Message);
break;
case NotificationType.Warning:
PrintWarnChat(msg.Message);
break;
case NotificationType.Error:
PrintErrorChat(msg.Message);
break;
}
}
private void ShowNotification(NotificationMessage msg)
{
Logger.LogInformation("{msg}", msg.ToString());
if (!_dalamudUtilService.IsLoggedIn) return;
switch (msg.Type)
{
case NotificationType.Info:
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
break;
case NotificationType.Warning:
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
break;
case NotificationType.Error:
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
break;
}
}
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowToast(NotificationMessage msg)
{
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
{
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info,
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
};
_notificationManager.AddNotification(new Notification()
{
Content = msg.Message ?? string.Empty,
Title = msg.Title,
Type = dalamudType,
Minimized = false,
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
});
}
}

View File

@@ -0,0 +1,199 @@
using MareSynchronos.MareConfiguration;
using MareSynchronos.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Globalization;
using System.Text;
namespace MareSynchronos.Services;
public sealed class PerformanceCollectorService : IHostedService
{
private const string _counterSplit = "=>";
private readonly ILogger<PerformanceCollectorService> _logger;
private readonly MareConfigService _mareConfigService;
public ConcurrentDictionary<string, RollingList<(TimeOnly, long)>> PerformanceCounters { get; } = new(StringComparer.Ordinal);
private readonly CancellationTokenSource _periodicLogPruneTaskCts = new();
public PerformanceCollectorService(ILogger<PerformanceCollectorService> logger, MareConfigService mareConfigService)
{
_logger = logger;
_mareConfigService = mareConfigService;
}
public T LogPerformance<T>(object sender, MareInterpolatedStringHandler counterName, Func<T> func, int maxEntries = 10000)
{
if (!_mareConfigService.Current.LogPerformance) return func.Invoke();
string cn = sender.GetType().Name + _counterSplit + counterName.BuildMessage();
if (!PerformanceCounters.TryGetValue(cn, out var list))
{
list = PerformanceCounters[cn] = new(maxEntries);
}
var dt = DateTime.UtcNow.Ticks;
try
{
return func.Invoke();
}
finally
{
var elapsed = DateTime.UtcNow.Ticks - dt;
#if DEBUG
if (TimeSpan.FromTicks(elapsed) > TimeSpan.FromMilliseconds(10))
_logger.LogWarning(">10ms spike on {counterName}: {time}", cn, TimeSpan.FromTicks(elapsed));
#endif
list.Add((TimeOnly.FromDateTime(DateTime.Now), elapsed));
}
}
public void LogPerformance(object sender, MareInterpolatedStringHandler counterName, Action act, int maxEntries = 10000)
{
if (!_mareConfigService.Current.LogPerformance) { act.Invoke(); return; }
var cn = sender.GetType().Name + _counterSplit + counterName.BuildMessage();
if (!PerformanceCounters.TryGetValue(cn, out var list))
{
list = PerformanceCounters[cn] = new(maxEntries);
}
var dt = DateTime.UtcNow.Ticks;
try
{
act.Invoke();
}
finally
{
var elapsed = DateTime.UtcNow.Ticks - dt;
#if DEBUG
if (TimeSpan.FromTicks(elapsed) > TimeSpan.FromMilliseconds(10))
_logger.LogWarning(">10ms spike on {counterName}: {time}", cn, TimeSpan.FromTicks(elapsed));
#endif
list.Add(new(TimeOnly.FromDateTime(DateTime.Now), elapsed));
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting PerformanceCollectorService");
_ = Task.Run(PeriodicLogPrune, _periodicLogPruneTaskCts.Token);
_logger.LogInformation("Started PerformanceCollectorService");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_periodicLogPruneTaskCts.Cancel();
_periodicLogPruneTaskCts.Dispose();
return Task.CompletedTask;
}
internal void PrintPerformanceStats(int limitBySeconds = 0)
{
if (!_mareConfigService.Current.LogPerformance)
{
_logger.LogWarning("Performance counters are disabled");
}
StringBuilder sb = new();
if (limitBySeconds > 0)
{
sb.AppendLine($"Performance Metrics over the past {limitBySeconds} seconds of each counter");
}
else
{
sb.AppendLine("Performance metrics over total lifetime of each counter");
}
var data = PerformanceCounters.ToList();
var longestCounterName = data.OrderByDescending(d => d.Key.Length).First().Key.Length + 2;
sb.Append("-Last".PadRight(15, '-'));
sb.Append('|');
sb.Append("-Max".PadRight(15, '-'));
sb.Append('|');
sb.Append("-Average".PadRight(15, '-'));
sb.Append('|');
sb.Append("-Last Update".PadRight(15, '-'));
sb.Append('|');
sb.Append("-Entries".PadRight(10, '-'));
sb.Append('|');
sb.Append("-Counter Name".PadRight(longestCounterName, '-'));
sb.AppendLine();
var orderedData = data.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToList();
var previousCaller = orderedData[0].Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0];
foreach (var entry in orderedData)
{
var newCaller = entry.Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0];
if (!string.Equals(previousCaller, newCaller, StringComparison.Ordinal))
{
DrawSeparator(sb, longestCounterName);
}
var pastEntries = limitBySeconds > 0 ? entry.Value.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList() : [.. entry.Value];
if (pastEntries.Any())
{
sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault() == default ? 0 : pastEntries.Last().Item2).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
sb.Append('|');
sb.Append((" " + TimeSpan.FromTicks(pastEntries.Max(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
sb.Append('|');
sb.Append((" " + TimeSpan.FromTicks((long)pastEntries.Average(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
sb.Append('|');
sb.Append((" " + (pastEntries.LastOrDefault() == default ? "-" : pastEntries.Last().Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' '));
sb.Append('|');
sb.Append((" " + pastEntries.Count).PadRight(10));
sb.Append('|');
sb.Append(' ').Append(entry.Key);
sb.AppendLine();
}
previousCaller = newCaller;
}
DrawSeparator(sb, longestCounterName);
_logger.LogInformation("{perf}", sb.ToString());
}
private static void DrawSeparator(StringBuilder sb, int longestCounterName)
{
sb.Append("".PadRight(15, '-'));
sb.Append('+');
sb.Append("".PadRight(15, '-'));
sb.Append('+');
sb.Append("".PadRight(15, '-'));
sb.Append('+');
sb.Append("".PadRight(15, '-'));
sb.Append('+');
sb.Append("".PadRight(10, '-'));
sb.Append('+');
sb.Append("".PadRight(longestCounterName, '-'));
sb.AppendLine();
}
private async Task PeriodicLogPrune()
{
while (!_periodicLogPruneTaskCts.Token.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(10), _periodicLogPruneTaskCts.Token).ConfigureAwait(false);
foreach (var entries in PerformanceCounters.ToList())
{
try
{
var last = entries.Value.ToList().Last();
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _))
{
_logger.LogDebug("Could not remove performance counter {counter}", entries.Key);
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Error removing performance counter {counter}", entries.Key);
}
}
}
}
}

View File

@@ -0,0 +1,236 @@
using MareSynchronos.API.Data;
using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Events;
using MareSynchronos.Services.Mediator;
using MareSynchronos.UI;
using MareSynchronos.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public class PlayerPerformanceService
{
private readonly FileCacheManager _fileCacheManager;
private readonly XivDataAnalyzer _xivDataAnalyzer;
private readonly ILogger<PlayerPerformanceService> _logger;
private readonly MareMediator _mediator;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, MareMediator mediator,
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
XivDataAnalyzer xivDataAnalyzer)
{
_logger = logger;
_mediator = mediator;
_playerPerformanceConfigService = playerPerformanceConfigService;
_fileCacheManager = fileCacheManager;
_xivDataAnalyzer = xivDataAnalyzer;
}
public async Task<bool> CheckBothThresholds(PairHandler pairHandler, CharacterData charaData)
{
var config = _playerPerformanceConfigService.Current;
bool notPausedAfterVram = ComputeAndAutoPauseOnVRAMUsageThresholds(pairHandler, charaData, []);
if (!notPausedAfterVram) return false;
bool notPausedAfterTris = await CheckTriangleUsageThresholds(pairHandler, charaData).ConfigureAwait(false);
if (!notPausedAfterTris) return false;
if (config.UIDsToIgnore
.Exists(uid => string.Equals(uid, pairHandler.Pair.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.Pair.UserData.UID, StringComparison.Ordinal)))
return true;
var vramUsage = pairHandler.Pair.LastAppliedApproximateVRAMBytes;
var triUsage = pairHandler.Pair.LastAppliedDataTris;
bool isPrefPerm = pairHandler.Pair.UserPair.OwnPermissions.HasFlag(API.Data.Enum.UserPermissions.Sticky);
bool exceedsTris = CheckForThreshold(config.WarnOnExceedingThresholds, config.TrisWarningThresholdThousands * 1000,
triUsage, config.WarnOnPreferredPermissionsExceedingThresholds, isPrefPerm);
bool exceedsVram = CheckForThreshold(config.WarnOnExceedingThresholds, config.VRAMSizeWarningThresholdMiB * 1024 * 1024,
vramUsage, config.WarnOnPreferredPermissionsExceedingThresholds, isPrefPerm);
if (_warnedForPlayers.TryGetValue(pairHandler.Pair.UserData.UID, out bool hadWarning) && hadWarning)
{
_warnedForPlayers[pairHandler.Pair.UserData.UID] = exceedsTris || exceedsVram;
return true;
}
_warnedForPlayers[pairHandler.Pair.UserData.UID] = exceedsTris || exceedsVram;
if (exceedsVram)
{
_mediator.Publish(new EventMessage(new Event(pairHandler.Pair.PlayerName, pairHandler.Pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
$"Exceeds VRAM threshold: ({UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeWarningThresholdMiB} MiB)")));
}
if (exceedsTris)
{
_mediator.Publish(new EventMessage(new Event(pairHandler.Pair.PlayerName, pairHandler.Pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
$"Exceeds triangle threshold: ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
}
if (exceedsTris || exceedsVram)
{
string warningText = string.Empty;
if (exceedsTris && !exceedsVram)
{
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured triangle warning threshold (" +
$"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles).";
}
else if (!exceedsTris)
{
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold (" +
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB).";
}
else
{
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds both VRAM warning threshold (" +
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB) and " +
$"triangle warning threshold ({triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles).";
}
_mediator.Publish(new NotificationMessage($"{pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds performance threshold(s)",
warningText, MareConfiguration.Models.NotificationType.Warning));
}
return true;
}
public async Task<bool> CheckTriangleUsageThresholds(PairHandler pairHandler, CharacterData charaData)
{
var config = _playerPerformanceConfigService.Current;
var pair = pairHandler.Pair;
long triUsage = 0;
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
{
pair.LastAppliedDataTris = 0;
return true;
}
var moddedModelHashes = playerReplacements.Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase)))
.Select(p => p.Hash)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (var hash in moddedModelHashes)
{
triUsage += await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
}
pair.LastAppliedDataTris = triUsage;
_logger.LogDebug("Calculated VRAM usage for {p}", pairHandler);
// no warning of any kind on ignored pairs
if (config.UIDsToIgnore
.Exists(uid => string.Equals(uid, pair.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pair.UserData.UID, StringComparison.Ordinal)))
return true;
bool isPrefPerm = pair.UserPair.OwnPermissions.HasFlag(API.Data.Enum.UserPermissions.Sticky);
// now check auto pause
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000,
triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
{
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold (" +
$"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)" +
$" and has been automatically paused.",
MareConfiguration.Models.NotificationType.Warning));
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
$"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
_mediator.Publish(new PauseMessage(pair.UserData));
return false;
}
return true;
}
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(PairHandler pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
{
var config = _playerPerformanceConfigService.Current;
var pair = pairHandler.Pair;
long vramUsage = 0;
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
{
pair.LastAppliedApproximateVRAMBytes = 0;
return true;
}
var moddedTextureHashes = playerReplacements.Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)))
.Select(p => p.Hash)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (var hash in moddedTextureHashes)
{
long fileSize = 0;
var download = toDownloadFiles.Find(f => string.Equals(hash, f.Hash, StringComparison.OrdinalIgnoreCase));
if (download != null)
{
fileSize = download.TotalRaw;
}
else
{
var fileEntry = _fileCacheManager.GetFileCacheByHash(hash);
if (fileEntry == null) continue;
if (fileEntry.Size == null)
{
fileEntry.Size = new FileInfo(fileEntry.ResolvedFilepath).Length;
_fileCacheManager.UpdateHashedFile(fileEntry, computeProperties: true);
}
fileSize = fileEntry.Size.Value;
}
vramUsage += fileSize;
}
pair.LastAppliedApproximateVRAMBytes = vramUsage;
_logger.LogDebug("Calculated VRAM usage for {p}", pairHandler);
// no warning of any kind on ignored pairs
if (config.UIDsToIgnore
.Exists(uid => string.Equals(uid, pair.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pair.UserData.UID, StringComparison.Ordinal)))
return true;
bool isPrefPerm = pair.UserPair.OwnPermissions.HasFlag(API.Data.Enum.UserPermissions.Sticky);
// now check auto pause
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024 * 1024,
vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
{
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold (" +
$"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB)" +
$" and has been automatically paused.",
MareConfiguration.Models.NotificationType.Warning));
_mediator.Publish(new PauseMessage(pair.UserData));
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
$"Exceeds VRAM threshold: automatically paused ({UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB} MiB)")));
return false;
}
return true;
}
private static bool CheckForThreshold(bool thresholdEnabled, long threshold, long value, bool checkForPrefPerm, bool isPrefPerm) =>
thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm);
}

View File

@@ -0,0 +1,76 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Comparer;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using System.Collections.Concurrent;
namespace MareSynchronos.PlayerData.Pairs;
public class PluginWarningNotificationService
{
private readonly ConcurrentDictionary<UserData, OptionalPluginWarning> _cachedOptionalPluginWarnings = new(UserDataComparer.Instance);
private readonly IpcManager _ipcManager;
private readonly MareConfigService _mareConfigService;
private readonly MareMediator _mediator;
public PluginWarningNotificationService(MareConfigService mareConfigService, IpcManager ipcManager, MareMediator mediator)
{
_mareConfigService = mareConfigService;
_ipcManager = ipcManager;
_mediator = mediator;
}
public void NotifyForMissingPlugins(UserData user, string playerName, HashSet<PlayerChanges> changes)
{
if (!_cachedOptionalPluginWarnings.TryGetValue(user, out var warning))
{
_cachedOptionalPluginWarnings[user] = warning = new()
{
ShownCustomizePlusWarning = _mareConfigService.Current.DisableOptionalPluginWarnings,
ShownHeelsWarning = _mareConfigService.Current.DisableOptionalPluginWarnings,
ShownHonorificWarning = _mareConfigService.Current.DisableOptionalPluginWarnings,
ShownMoodlesWarning = _mareConfigService.Current.DisableOptionalPluginWarnings,
ShowPetNicknamesWarning = _mareConfigService.Current.DisableOptionalPluginWarnings
};
}
List<string> missingPluginsForData = [];
if (changes.Contains(PlayerChanges.Heels) && !warning.ShownHeelsWarning && !_ipcManager.Heels.APIAvailable)
{
missingPluginsForData.Add("SimpleHeels");
warning.ShownHeelsWarning = true;
}
if (changes.Contains(PlayerChanges.Customize) && !warning.ShownCustomizePlusWarning && !_ipcManager.CustomizePlus.APIAvailable)
{
missingPluginsForData.Add("Customize+");
warning.ShownCustomizePlusWarning = true;
}
if (changes.Contains(PlayerChanges.Honorific) && !warning.ShownHonorificWarning && !_ipcManager.Honorific.APIAvailable)
{
missingPluginsForData.Add("Honorific");
warning.ShownHonorificWarning = true;
}
if (changes.Contains(PlayerChanges.Moodles) && !warning.ShownMoodlesWarning && !_ipcManager.Moodles.APIAvailable)
{
missingPluginsForData.Add("Moodles");
warning.ShownMoodlesWarning = true;
}
if (changes.Contains(PlayerChanges.PetNames) && !warning.ShowPetNicknamesWarning && !_ipcManager.PetNames.APIAvailable)
{
missingPluginsForData.Add("PetNicknames");
warning.ShowPetNicknamesWarning = true;
}
if (missingPluginsForData.Any())
{
_mediator.Publish(new NotificationMessage("Missing plugins for " + playerName,
$"Received data for {playerName} that contained information for plugins you have not installed. Install {string.Join(", ", missingPluginsForData)} to experience their character fully.",
NotificationType.Warning, TimeSpan.FromSeconds(10)));
}
}
}

View File

@@ -0,0 +1,582 @@
using Dalamud.Utility;
using MareSynchronos.API.Routes;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json;
namespace MareSynchronos.Services.ServerConfiguration;
public class ServerConfigurationManager
{
private readonly ServerConfigService _configService;
private readonly DalamudUtilService _dalamudUtil;
private readonly MareConfigService _mareConfigService;
private readonly HttpClient _httpClient;
private readonly ILogger<ServerConfigurationManager> _logger;
private readonly MareMediator _mareMediator;
private readonly NotesConfigService _notesConfig;
private readonly ServerTagConfigService _serverTagConfig;
public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil,
MareConfigService mareConfigService, HttpClient httpClient, MareMediator mareMediator)
{
_logger = logger;
_configService = configService;
_serverTagConfig = serverTagConfig;
_notesConfig = notesConfig;
_dalamudUtil = dalamudUtil;
_mareConfigService = mareConfigService;
_httpClient = httpClient;
_mareMediator = mareMediator;
EnsureMainExists();
}
public string CurrentApiUrl => CurrentServer.ServerUri;
public ServerStorage CurrentServer => _configService.Current.ServerStorage[CurrentServerIndex];
public bool SendCensusData
{
get
{
return _configService.Current.SendCensusData;
}
set
{
_configService.Current.SendCensusData = value;
_configService.Save();
}
}
public bool ShownCensusPopup
{
get
{
return _configService.Current.ShownCensusPopup;
}
set
{
_configService.Current.ShownCensusPopup = value;
_configService.Save();
}
}
public int CurrentServerIndex
{
set
{
_configService.Current.CurrentServer = value;
_configService.Save();
}
get
{
if (_configService.Current.CurrentServer < 0)
{
_configService.Current.CurrentServer = 0;
_configService.Save();
}
return _configService.Current.CurrentServer;
}
}
public (string OAuthToken, string UID)? GetOAuth2(out bool hasMulti, int serverIdx = -1)
{
ServerStorage? currentServer;
currentServer = serverIdx == -1 ? CurrentServer : GetServerByIndex(serverIdx);
if (currentServer == null)
{
currentServer = new();
Save();
}
hasMulti = false;
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult();
var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName) && f.WorldId == worldId);
if (auth.Count >= 2)
{
_logger.LogTrace("GetOAuth2 accessed, returning null because multiple ({count}) identical characters.", auth.Count);
hasMulti = true;
return null;
}
if (auth.Count == 0)
{
_logger.LogTrace("GetOAuth2 accessed, returning null because no set up characters for {chara} on {world}", charaName, worldId);
return null;
}
if (auth.Single().LastSeenCID != cid)
{
auth.Single().LastSeenCID = cid;
_logger.LogTrace("GetOAuth2 accessed, updating CID for {chara} on {world} to {cid}", charaName, worldId, cid);
Save();
}
if (!string.IsNullOrEmpty(auth.Single().UID) && !string.IsNullOrEmpty(currentServer.OAuthToken))
{
_logger.LogTrace("GetOAuth2 accessed, returning {key} ({keyValue}) for {chara} on {world}", auth.Single().UID, string.Join("", currentServer.OAuthToken.Take(10)), charaName, worldId);
return (currentServer.OAuthToken, auth.Single().UID!);
}
_logger.LogTrace("GetOAuth2 accessed, returning null because no UID found for {chara} on {world} or OAuthToken is not configured.", charaName, worldId);
return null;
}
public string? GetSecretKey(out bool hasMulti, int serverIdx = -1)
{
ServerStorage? currentServer;
currentServer = serverIdx == -1 ? CurrentServer : GetServerByIndex(serverIdx);
if (currentServer == null)
{
currentServer = new();
Save();
}
hasMulti = false;
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult();
if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any())
{
currentServer.Authentications.Add(new Authentication()
{
CharacterName = charaName,
WorldId = worldId,
LastSeenCID = cid,
SecretKeyIdx = currentServer.SecretKeys.Last().Key,
});
Save();
}
var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId);
if (auth.Count >= 2)
{
_logger.LogTrace("GetSecretKey accessed, returning null because multiple ({count}) identical characters.", auth.Count);
hasMulti = true;
return null;
}
if (auth.Count == 0)
{
_logger.LogTrace("GetSecretKey accessed, returning null because no set up characters for {chara} on {world}", charaName, worldId);
return null;
}
if (auth.Single().LastSeenCID != cid)
{
auth.Single().LastSeenCID = cid;
_logger.LogTrace("GetSecretKey accessed, updating CID for {chara} on {world} to {cid}", charaName, worldId, cid);
Save();
}
if (currentServer.SecretKeys.TryGetValue(auth.Single().SecretKeyIdx, out var secretKey))
{
_logger.LogTrace("GetSecretKey accessed, returning {key} ({keyValue}) for {chara} on {world}", secretKey.FriendlyName, string.Join("", secretKey.Key.Take(10)), charaName, worldId);
return secretKey.Key;
}
_logger.LogTrace("GetSecretKey accessed, returning null because no fitting key found for {chara} on {world} for idx {idx}.", charaName, worldId, auth.Single().SecretKeyIdx);
return null;
}
public string[] GetServerApiUrls()
{
return _configService.Current.ServerStorage.Select(v => v.ServerUri).ToArray();
}
public ServerStorage GetServerByIndex(int idx)
{
try
{
return _configService.Current.ServerStorage[idx];
}
catch
{
_configService.Current.CurrentServer = 0;
EnsureMainExists();
return CurrentServer!;
}
}
public string GetDiscordUserFromToken(ServerStorage server)
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
if (string.IsNullOrEmpty(server.OAuthToken)) return string.Empty;
try
{
var token = handler.ReadJwtToken(server.OAuthToken);
return token.Claims.First(f => string.Equals(f.Type, "discord_user", StringComparison.Ordinal)).Value!;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not read jwt, resetting it");
server.OAuthToken = null;
Save();
return string.Empty;
}
}
public string[] GetServerNames()
{
return _configService.Current.ServerStorage.Select(v => v.ServerName).ToArray();
}
public bool HasValidConfig()
{
return CurrentServer != null && CurrentServer.Authentications.Count > 0;
}
public void Save()
{
var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown";
_logger.LogDebug("{caller} Calling config save", caller);
_configService.Save();
}
public void SelectServer(int idx)
{
_configService.Current.CurrentServer = idx;
CurrentServer!.FullPause = false;
Save();
}
internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1)
{
if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex;
var server = GetServerByIndex(serverSelectionIndex);
if (server.Authentications.Any(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal)
&& c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult()))
return;
server.Authentications.Add(new Authentication()
{
CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(),
WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(),
SecretKeyIdx = !server.UseOAuth2 ? server.SecretKeys.Last().Key : -1,
LastSeenCID = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult()
});
Save();
}
internal void AddEmptyCharacterToServer(int serverSelectionIndex)
{
var server = GetServerByIndex(serverSelectionIndex);
server.Authentications.Add(new Authentication()
{
SecretKeyIdx = server.SecretKeys.Any() ? server.SecretKeys.First().Key : -1,
});
Save();
}
internal void AddOpenPairTag(string tag)
{
CurrentServerTagStorage().OpenPairTags.Add(tag);
_serverTagConfig.Save();
}
internal void AddServer(ServerStorage serverStorage)
{
_configService.Current.ServerStorage.Add(serverStorage);
Save();
}
internal void AddTag(string tag)
{
CurrentServerTagStorage().ServerAvailablePairTags.Add(tag);
_serverTagConfig.Save();
_mareMediator.Publish(new RefreshUiMessage());
}
internal void AddTagForUid(string uid, string tagName)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
tags.Add(tagName);
_mareMediator.Publish(new RefreshUiMessage());
}
else
{
CurrentServerTagStorage().UidServerPairedUserTags[uid] = [tagName];
}
_serverTagConfig.Save();
}
internal bool ContainsOpenPairTag(string tag)
{
return CurrentServerTagStorage().OpenPairTags.Contains(tag);
}
internal bool ContainsTag(string uid, string tag)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
return tags.Contains(tag, StringComparer.Ordinal);
}
return false;
}
internal void DeleteServer(ServerStorage selectedServer)
{
if (Array.IndexOf(_configService.Current.ServerStorage.ToArray(), selectedServer) <
_configService.Current.CurrentServer)
{
_configService.Current.CurrentServer--;
}
_configService.Current.ServerStorage.Remove(selectedServer);
Save();
}
internal string? GetNoteForGid(string gID)
{
if (CurrentNotesStorage().GidServerComments.TryGetValue(gID, out var note))
{
if (string.IsNullOrEmpty(note)) return null;
return note;
}
return null;
}
internal string? GetNoteForUid(string uid)
{
if (CurrentNotesStorage().UidServerComments.TryGetValue(uid, out var note))
{
if (string.IsNullOrEmpty(note)) return null;
return note;
}
return null;
}
internal HashSet<string> GetServerAvailablePairTags()
{
return CurrentServerTagStorage().ServerAvailablePairTags;
}
internal Dictionary<string, List<string>> GetUidServerPairedUserTags()
{
return CurrentServerTagStorage().UidServerPairedUserTags;
}
internal HashSet<string> GetUidsForTag(string tag)
{
return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
}
internal bool HasTags(string uid)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
return tags.Any();
}
return false;
}
internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item)
{
var server = GetServerByIndex(serverSelectionIndex);
server.Authentications.Remove(item);
Save();
}
internal void RemoveOpenPairTag(string tag)
{
CurrentServerTagStorage().OpenPairTags.Remove(tag);
_serverTagConfig.Save();
}
internal void RemoveTag(string tag)
{
CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag);
foreach (var uid in GetUidsForTag(tag))
{
RemoveTagForUid(uid, tag, save: false);
}
_serverTagConfig.Save();
_mareMediator.Publish(new RefreshUiMessage());
}
internal void RemoveTagForUid(string uid, string tagName, bool save = true)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
tags.Remove(tagName);
if (save)
{
_serverTagConfig.Save();
_mareMediator.Publish(new RefreshUiMessage());
}
}
}
internal void RenameTag(string oldName, string newName)
{
CurrentServerTagStorage().ServerAvailablePairTags.Remove(oldName);
CurrentServerTagStorage().ServerAvailablePairTags.Add(newName);
foreach (var existingTags in CurrentServerTagStorage().UidServerPairedUserTags.Select(k => k.Value))
{
if (existingTags.Remove(oldName))
existingTags.Add(newName);
}
}
internal void SaveNotes()
{
_notesConfig.Save();
}
internal void SetNoteForGid(string gid, string note, bool save = true)
{
if (string.IsNullOrEmpty(gid)) return;
CurrentNotesStorage().GidServerComments[gid] = note;
if (save)
_notesConfig.Save();
}
internal void SetNoteForUid(string uid, string note, bool save = true)
{
if (string.IsNullOrEmpty(uid)) return;
CurrentNotesStorage().UidServerComments[uid] = note;
if (save)
_notesConfig.Save();
}
internal void AutoPopulateNoteForUid(string uid, string note)
{
if (!_mareConfigService.Current.AutoPopulateEmptyNotesFromCharaName
|| GetNoteForUid(uid) != null)
return;
SetNoteForUid(uid, note, save: true);
}
private ServerNotesStorage CurrentNotesStorage()
{
TryCreateCurrentNotesStorage();
return _notesConfig.Current.ServerNotes[CurrentApiUrl];
}
private ServerTagStorage CurrentServerTagStorage()
{
TryCreateCurrentServerTagStorage();
return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl];
}
private void EnsureMainExists()
{
if (_configService.Current.ServerStorage.Count == 0 || !string.Equals(_configService.Current.ServerStorage[0].ServerUri, ApiController.MainServiceUri, StringComparison.OrdinalIgnoreCase))
{
_configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.MainServiceUri, ServerName = ApiController.MainServer, UseOAuth2 = true });
}
Save();
}
private void TryCreateCurrentNotesStorage()
{
if (!_notesConfig.Current.ServerNotes.ContainsKey(CurrentApiUrl))
{
_notesConfig.Current.ServerNotes[CurrentApiUrl] = new();
}
}
private void TryCreateCurrentServerTagStorage()
{
if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
{
_serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
}
}
public async Task<Dictionary<string, string>> GetUIDsWithDiscordToken(string serverUri, string token)
{
try
{
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
var oauthCheckUri = MareAuth.GetUIDsFullPath(new Uri(baseUri));
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.GetAsync(oauthCheckUri).ConfigureAwait(false);
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failure getting UIDs");
return [];
}
}
public async Task<Uri?> CheckDiscordOAuth(string serverUri)
{
try
{
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
var oauthCheckUri = MareAuth.GetDiscordOAuthEndpointFullPath(new Uri(baseUri));
var response = await _httpClient.GetFromJsonAsync<Uri?>(oauthCheckUri).ConfigureAwait(false);
return response;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failure checking for Discord Auth");
return null;
}
}
public async Task<string?> GetDiscordOAuthToken(Uri discordAuthUri, string serverUri, CancellationToken token)
{
var sessionId = BitConverter.ToString(RandomNumberGenerator.GetBytes(64)).Replace("-", "").ToLower();
Util.OpenLink(discordAuthUri.ToString() + "?sessionId=" + sessionId);
string? discordToken = null;
using CancellationTokenSource timeOutCts = new();
timeOutCts.CancelAfter(TimeSpan.FromSeconds(60));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeOutCts.Token, token);
try
{
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
var oauthCheckUri = MareAuth.GetDiscordOAuthTokenFullPath(new Uri(baseUri), sessionId);
var response = await _httpClient.GetAsync(oauthCheckUri, linkedCts.Token).ConfigureAwait(false);
discordToken = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failure getting Discord Token");
return null;
}
if (discordToken == null)
return null;
return discordToken;
}
public HttpTransportType GetTransport()
{
return CurrentServer.HttpTransportType;
}
public void SetTransportType(HttpTransportType httpTransportType)
{
CurrentServer.HttpTransportType = httpTransportType;
Save();
}
}

View File

@@ -0,0 +1,54 @@
using MareSynchronos.API.Dto.Group;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI;
using MareSynchronos.UI.Components.Popup;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public class UiFactory
{
private readonly ILoggerFactory _loggerFactory;
private readonly MareMediator _mareMediator;
private readonly ApiController _apiController;
private readonly UiSharedService _uiSharedService;
private readonly PairManager _pairManager;
private readonly ServerConfigurationManager _serverConfigManager;
private readonly MareProfileManager _mareProfileManager;
private readonly PerformanceCollectorService _performanceCollectorService;
public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController,
UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager,
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
{
_loggerFactory = loggerFactory;
_mareMediator = mareMediator;
_apiController = apiController;
_uiSharedService = uiSharedService;
_pairManager = pairManager;
_serverConfigManager = serverConfigManager;
_mareProfileManager = mareProfileManager;
_performanceCollectorService = performanceCollectorService;
}
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
{
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _mareMediator,
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService);
}
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
{
return new StandaloneProfileUi(_loggerFactory.CreateLogger<StandaloneProfileUi>(), _mareMediator,
_uiSharedService, _serverConfigManager, _mareProfileManager, _pairManager, pair, _performanceCollectorService);
}
public PermissionWindowUI CreatePermissionPopupUi(Pair pair)
{
return new PermissionWindowUI(_loggerFactory.CreateLogger<PermissionWindowUI>(), pair,
_mareMediator, _uiSharedService, _apiController, _performanceCollectorService);
}
}

View File

@@ -0,0 +1,126 @@
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.Mediator;
using MareSynchronos.UI;
using MareSynchronos.UI.Components.Popup;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public sealed class UiService : DisposableMediatorSubscriberBase
{
private readonly List<WindowMediatorSubscriberBase> _createdWindows = [];
private readonly IUiBuilder _uiBuilder;
private readonly FileDialogManager _fileDialogManager;
private readonly ILogger<UiService> _logger;
private readonly MareConfigService _mareConfigService;
private readonly WindowSystem _windowSystem;
private readonly UiFactory _uiFactory;
public UiService(ILogger<UiService> logger, IUiBuilder uiBuilder,
MareConfigService mareConfigService, WindowSystem windowSystem,
IEnumerable<WindowMediatorSubscriberBase> windows,
UiFactory uiFactory, FileDialogManager fileDialogManager,
MareMediator mareMediator) : base(logger, mareMediator)
{
_logger = logger;
_logger.LogTrace("Creating {type}", GetType().Name);
_uiBuilder = uiBuilder;
_mareConfigService = mareConfigService;
_windowSystem = windowSystem;
_uiFactory = uiFactory;
_fileDialogManager = fileDialogManager;
_uiBuilder.DisableGposeUiHide = true;
_uiBuilder.Draw += Draw;
_uiBuilder.OpenConfigUi += ToggleUi;
_uiBuilder.OpenMainUi += ToggleMainUi;
foreach (var window in windows)
{
_windowSystem.AddWindow(window);
}
Mediator.Subscribe<ProfileOpenStandaloneMessage>(this, (msg) =>
{
if (!_createdWindows.Exists(p => p is StandaloneProfileUi ui
&& string.Equals(ui.Pair.UserData.AliasOrUID, msg.Pair.UserData.AliasOrUID, StringComparison.Ordinal)))
{
var window = _uiFactory.CreateStandaloneProfileUi(msg.Pair);
_createdWindows.Add(window);
_windowSystem.AddWindow(window);
}
});
Mediator.Subscribe<OpenSyncshellAdminPanel>(this, (msg) =>
{
if (!_createdWindows.Exists(p => p is SyncshellAdminUI ui
&& string.Equals(ui.GroupFullInfo.GID, msg.GroupInfo.GID, StringComparison.Ordinal)))
{
var window = _uiFactory.CreateSyncshellAdminUi(msg.GroupInfo);
_createdWindows.Add(window);
_windowSystem.AddWindow(window);
}
});
Mediator.Subscribe<OpenPermissionWindow>(this, (msg) =>
{
if (!_createdWindows.Exists(p => p is PermissionWindowUI ui
&& msg.Pair == ui.Pair))
{
var window = _uiFactory.CreatePermissionPopupUi(msg.Pair);
_createdWindows.Add(window);
_windowSystem.AddWindow(window);
}
});
Mediator.Subscribe<RemoveWindowMessage>(this, (msg) =>
{
_windowSystem.RemoveWindow(msg.Window);
_createdWindows.Remove(msg.Window);
msg.Window.Dispose();
});
}
public void ToggleMainUi()
{
if (_mareConfigService.Current.HasValidSetup())
Mediator.Publish(new UiToggleMessage(typeof(CompactUi)));
else
Mediator.Publish(new UiToggleMessage(typeof(IntroUi)));
}
public void ToggleUi()
{
if (_mareConfigService.Current.HasValidSetup())
Mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
else
Mediator.Publish(new UiToggleMessage(typeof(IntroUi)));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_logger.LogTrace("Disposing {type}", GetType().Name);
_windowSystem.RemoveAllWindows();
foreach (var window in _createdWindows)
{
window.Dispose();
}
_uiBuilder.Draw -= Draw;
_uiBuilder.OpenConfigUi -= ToggleUi;
_uiBuilder.OpenMainUi -= ToggleMainUi;
}
private void Draw()
{
_windowSystem.Draw();
_fileDialogManager.Draw();
}
}

View File

@@ -0,0 +1,214 @@
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.Havok.Animation;
using FFXIVClientStructs.Havok.Common.Base.Types;
using FFXIVClientStructs.Havok.Common.Serialize.Util;
using MareSynchronos.FileCache;
using MareSynchronos.Interop.GameModel;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
namespace MareSynchronos.Services;
public sealed class XivDataAnalyzer
{
private readonly ILogger<XivDataAnalyzer> _logger;
private readonly FileCacheManager _fileCacheManager;
private readonly XivDataStorageService _configService;
private readonly List<string> _failedCalculatedTris = [];
public XivDataAnalyzer(ILogger<XivDataAnalyzer> logger, FileCacheManager fileCacheManager,
XivDataStorageService configService)
{
_logger = logger;
_fileCacheManager = fileCacheManager;
_configService = configService;
}
public unsafe Dictionary<string, List<ushort>>? GetSkeletonBoneIndices(GameObjectHandler handler)
{
if (handler.Address == nint.Zero) return null;
var chara = (CharacterBase*)(((Character*)handler.Address)->GameObject.DrawObject);
if (chara->GetModelType() != CharacterBase.ModelType.Human) return null;
var resHandles = chara->Skeleton->SkeletonResourceHandles;
Dictionary<string, List<ushort>> outputIndices = [];
try
{
for (int i = 0; i < chara->Skeleton->PartialSkeletonCount; i++)
{
var handle = *(resHandles + i);
_logger.LogTrace("Iterating over SkeletonResourceHandle #{i}:{x}", i, ((nint)handle).ToString("X"));
if ((nint)handle == nint.Zero) continue;
var curBones = handle->BoneCount;
// this is unrealistic, the filename shouldn't ever be that long
if (handle->FileName.Length > 1024) continue;
var skeletonName = handle->FileName.ToString();
if (string.IsNullOrEmpty(skeletonName)) continue;
outputIndices[skeletonName] = new();
for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++)
{
var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String;
if (boneName == null) continue;
outputIndices[skeletonName].Add((ushort)(boneIdx + 1));
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not process skeleton data");
}
return (outputIndices.Count != 0 && outputIndices.Values.All(u => u.Count > 0)) ? outputIndices : null;
}
public unsafe Dictionary<string, List<ushort>>? GetBoneIndicesFromPap(string hash)
{
if (_configService.Current.BonesDictionary.TryGetValue(hash, out var bones)) return bones;
var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash);
if (cacheEntity == null) return null;
using BinaryReader reader = new BinaryReader(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read));
// most of this shit is from vfxeditor, surely nothing will change in the pap format :copium:
reader.ReadInt32(); // ignore
reader.ReadInt32(); // ignore
reader.ReadInt16(); // read 2 (num animations)
reader.ReadInt16(); // read 2 (modelid)
var type = reader.ReadByte();// read 1 (type)
if (type != 0) return null; // it's not human, just ignore it, whatever
reader.ReadByte(); // read 1 (variant)
reader.ReadInt32(); // ignore
var havokPosition = reader.ReadInt32();
var footerPosition = reader.ReadInt32();
var havokDataSize = footerPosition - havokPosition;
reader.BaseStream.Position = havokPosition;
var havokData = reader.ReadBytes(havokDataSize);
if (havokData.Length <= 8) return null; // no havok data
var output = new Dictionary<string, List<ushort>>(StringComparer.OrdinalIgnoreCase);
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + ".hkx";
var tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
try
{
File.WriteAllBytes(tempHavokDataPath, havokData);
var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1];
loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
{
Storage = (int)(hkSerializeUtil.LoadOptionBits.Default)
};
var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions);
if (resource == null)
{
throw new InvalidOperationException("Resource was null after loading");
}
var rootLevelName = @"hkRootLevelContainer"u8;
fixed (byte* n1 = rootLevelName)
{
var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
var animationName = @"hkaAnimationContainer"u8;
fixed (byte* n2 = animationName)
{
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
for (int i = 0; i < animContainer->Bindings.Length; i++)
{
var binding = animContainer->Bindings[i].ptr;
var boneTransform = binding->TransformTrackToBoneIndices;
string name = binding->OriginalSkeletonName.String! + "_" + i;
output[name] = [];
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
{
output[name].Add((ushort)boneTransform[boneIdx]);
}
output[name].Sort();
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath);
}
finally
{
Marshal.FreeHGlobal(tempHavokDataPathAnsi);
File.Delete(tempHavokDataPath);
}
_configService.Current.BonesDictionary[hash] = output;
_configService.Save();
return output;
}
public async Task<long> GetTrianglesByHash(string hash)
{
if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
return cachedTris;
if (_failedCalculatedTris.Contains(hash, StringComparer.Ordinal))
return 0;
var path = _fileCacheManager.GetFileCacheByHash(hash);
if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
return 0;
var filePath = path.ResolvedFilepath;
try
{
_logger.LogDebug("Detected Model File {path}, calculating Tris", filePath);
var file = new MdlFile(filePath);
if (file.LodCount <= 0)
{
_failedCalculatedTris.Add(hash);
_configService.Current.TriangleDictionary[hash] = 0;
_configService.Save();
return 0;
}
long tris = 0;
for (int i = 0; i < file.LodCount; i++)
{
try
{
var meshIdx = file.Lods[i].MeshIndex;
var meshCnt = file.Lods[i].MeshCount;
tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", i, filePath);
continue;
}
if (tris > 0)
{
_logger.LogDebug("TriAnalysis: {filePath} => {tris} triangles", filePath, tris);
_configService.Current.TriangleDictionary[hash] = tris;
_configService.Save();
break;
}
}
return tris;
}
catch (Exception e)
{
_failedCalculatedTris.Add(hash);
_configService.Current.TriangleDictionary[hash] = 0;
_configService.Save();
_logger.LogWarning(e, "Could not parse file {file}", filePath);
return 0;
}
}
}