This commit is contained in:
2025-08-22 02:19:48 +01:00
commit a4c82452be
373 changed files with 52044 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
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 NoSnapService _noSnapService;
private readonly Dictionary<string, HandledCharaDataEntry> _handledCharaData = new(StringComparer.Ordinal);
public IReadOnlyDictionary<string, HandledCharaDataEntry> HandledCharaData => _handledCharaData;
public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator,
GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService,
IpcManager ipcManager, NoSnapService noSnapService)
: base(logger, mediator)
{
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_dalamudUtilService = dalamudUtilService;
_ipcManager = ipcManager;
_noSnapService = noSnapService;
mediator.Subscribe<GposeEndMessage>(this, msg =>
{
foreach (var chara in _handledCharaData)
{
_ = RevertHandledChara(chara.Value);
}
});
mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (_) => HandleCutsceneFrameworkUpdate());
}
private void HandleCutsceneFrameworkUpdate()
{
if (!_dalamudUtilService.IsInGpose) return;
foreach (var entry in _handledCharaData.Values.ToList())
{
var chara = _dalamudUtilService.GetGposeCharacterFromObjectTableByName(entry.Name, onlyGposeCharacters: true);
if (chara is null)
{
_handledCharaData.Remove(entry.Name);
_ = _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(entry.Name, entry.CustomizePlus));
}
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
foreach (var chara in _handledCharaData.Values)
{
_ = RevertHandledChara(chara);
}
}
public HandledCharaDataEntry? GetHandledCharacter(string name)
{
return _handledCharaData.GetValueOrDefault(name);
}
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.GetValueOrDefault(name);
return await RevertHandledChara(handled).ConfigureAwait(false);
}
public async Task<bool> RevertHandledChara(HandledCharaDataEntry? handled)
{
if (handled == null) return false;
_handledCharaData.Remove(handled.Name);
await _dalamudUtilService.RunOnFrameworkThread(async () =>
{
RemoveGposer(handled);
await RevertChara(handled.Name, handled.CustomizePlus).ConfigureAwait(false);
}).ConfigureAwait(false);
return true;
}
internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry)
{
_handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry);
_ = _dalamudUtilService.RunOnFrameworkThread(() => AddGposer(handledCharaDataEntry));
}
public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
{
foreach (var handledData in _handledCharaData.Values)
{
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;
}
private int GetGposerObjectIndex(string name)
{
return _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, _dalamudUtilService.IsInGpose)?.ObjectIndex ?? -1;
}
private void AddGposer(HandledCharaDataEntry handled)
{
int objectIndex = GetGposerObjectIndex(handled.Name);
if (objectIndex > 0)
_noSnapService.AddGposer(objectIndex);
}
private void RemoveGposer(HandledCharaDataEntry handled)
{
int objectIndex = GetGposerObjectIndex(handled.Name);
if (objectIndex > 0)
_noSnapService.RemoveGposer(objectIndex);
}
}

View File

@@ -0,0 +1,302 @@
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();
await _playerDataFactory.BuildCharacterData(newCdata, tempHandler, CancellationToken.None).ConfigureAwait(false);
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(2), 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 ? 2 : 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,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,362 @@
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 (AccessType == AccessTypeDto.Public && ShareType == ShareTypeDto.Shared)
{
ShareType = ShareTypeDto.Private;
}
if (Equals(base.AccessType, _charaDataFullDto.AccessType))
{
base.AccessType = null;
}
}
}
public new ShareTypeDto ShareType
{
get
{
return base.ShareType ?? _charaDataFullDto.ShareType;
}
set
{
base.ShareType = value;
if (ShareType == ShareTypeDto.Shared && AccessType == AccessTypeDto.Public)
{
base.ShareType = ShareTypeDto.Private;
}
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;
}
}