Initial
This commit is contained in:
156
MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs
Normal file
156
MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
302
MareSynchronos/Services/CharaData/CharaDataFileHandler.cs
Normal file
302
MareSynchronos/Services/CharaData/CharaDataFileHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
1022
MareSynchronos/Services/CharaData/CharaDataManager.cs
Normal file
1022
MareSynchronos/Services/CharaData/CharaDataManager.cs
Normal file
File diff suppressed because it is too large
Load Diff
296
MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
Normal file
296
MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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; }
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
174
MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs
Normal file
174
MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs
Normal 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();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user