Add MCDO (#80)

* update api

* mcd online editor impl

* most of chara data hub impl

* some state of things

* some refactoring

* random bullshit go

* more nearby impl

* add uid to peformance msg

* cleanup/homogenization

* some split, update nuget packages

* migrate to latest packages where possible, remove lz4net, do some split, idk

* some polish and cleanup

* more cleanup, beautification, etc.

* fixes and cleanups

---------

Co-authored-by: Stanley Dimant <root.darkarchon@outlook.com>
This commit is contained in:
rootdarkarchon
2025-01-11 22:43:11 +01:00
committed by Loporrit
parent ad42b29a44
commit 30caedbf3a
44 changed files with 5128 additions and 486 deletions

View File

@@ -0,0 +1,141 @@
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;
internal sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
{
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly DalamudUtilService _dalamudUtilService;
private readonly IpcManager _ipcManager;
private readonly HashSet<HandledCharaDataEntry> _handledCharaData = [];
public IEnumerable<HandledCharaDataEntry> HandledCharaData => _handledCharaData;
public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator,
GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService,
IpcManager ipcManager)
: base(logger, mediator)
{
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_dalamudUtilService = dalamudUtilService;
_ipcManager = ipcManager;
mediator.Subscribe<GposeEndMessage>(this, (_) =>
{
foreach (var chara in _handledCharaData)
{
RevertHandledChara(chara, false);
}
});
mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (_) => HandleCutsceneFrameworkUpdate());
}
private void HandleCutsceneFrameworkUpdate()
{
if (!_dalamudUtilService.IsInGpose) return;
foreach (var entry in _handledCharaData.ToList())
{
var chara = _dalamudUtilService.GetGposeCharacterFromObjectTableByName(entry.Name, onlyGposeCharacters: true);
if (chara is null)
{
RevertChara(entry.Name, entry.CustomizePlus).GetAwaiter().GetResult();
_handledCharaData.Remove(entry);
}
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
foreach (var chara in _handledCharaData)
{
RevertHandledChara(chara, false);
}
}
public async Task RevertChara(string name, Guid? cPlusId, bool reapplyPose = true)
{
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 != IntPtr.Zero)
{
var poseData = string.Empty;
API.Dto.CharaData.WorldData? worldData = null;
if (_dalamudUtilService.IsInGpose && reapplyPose)
{
poseData = await _ipcManager.Brio.GetPoseAsync(handler.Address).ConfigureAwait(false);
worldData = await _ipcManager.Brio.GetTransformAsync(handler.Address).ConfigureAwait(false);
}
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, CancellationToken.None).ConfigureAwait(false);
if (_dalamudUtilService.IsInGpose && reapplyPose)
{
await _ipcManager.Brio.SetPoseAsync(handler.Address, poseData ?? "{}").ConfigureAwait(false);
await _ipcManager.Brio.ApplyTransformAsync(handler.Address, worldData!.Value).ConfigureAwait(false);
}
}
}
public async Task<bool> RevertHandledChara(string name, bool reapplyPose = true)
{
var handled = _handledCharaData.FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.Ordinal));
if (handled == null) return false;
_handledCharaData.Remove(handled);
await _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(handled.Name, handled.CustomizePlus, reapplyPose)).ConfigureAwait(false);
return true;
}
public Task RevertHandledChara(HandledCharaDataEntry? handled, bool reapplyPose = true)
{
if (handled == null) return Task.CompletedTask;
_handledCharaData.Remove(handled);
return _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(handled.Name, handled.CustomizePlus, reapplyPose));
}
internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry)
{
_handledCharaData.Add(handledCharaDataEntry);
}
public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
{
foreach (var handledData in _handledCharaData)
{
if (newData.TryGetValue(handledData.MetaInfo.FullId, out var metaInfo) && metaInfo != null)
{
handledData.MetaInfo = metaInfo;
}
}
}
public async Task<GameObjectHandler?> TryCreateGameObjectHandler(string name, bool gPoseOnly = false)
{
var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
() => _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, gPoseOnly && _dalamudUtilService.IsInGpose)?.Address ?? IntPtr.Zero, false)
.ConfigureAwait(false);
if (handler.Address == nint.Zero) return null;
return handler;
}
public async Task<GameObjectHandler?> TryCreateGameObjectHandler(int index)
{
var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
() => _dalamudUtilService.GetCharacterFromObjectTableByIndex(index)?.Address ?? IntPtr.Zero, false)
.ConfigureAwait(false);
if (handler.Address == nint.Zero) return null;
return handler;
}
}

View File

@@ -0,0 +1,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;
internal 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,937 @@
using Dalamud.Game.ClientState.Objects.Types;
using K4os.Compression.LZ4.Legacy;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Factories;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.CharaData.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using System.Text;
namespace MareSynchronos.Services;
internal sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly CharaDataConfigService _configService;
private readonly DalamudUtilService _dalamudUtilService;
private readonly CharaDataFileHandler _fileHandler;
private readonly IpcManager _ipcManager;
private readonly Dictionary<string, CharaDataMetaInfoExtendedDto?> _metaInfoCache = [];
private readonly List<CharaDataMetaInfoExtendedDto> _nearbyData = [];
private readonly CharaDataNearbyManager _nearbyManager;
private readonly CharaDataCharacterHandler _characterHandler;
private readonly Dictionary<string, CharaDataFullExtendedDto> _ownCharaData = [];
private readonly Dictionary<string, Task> _sharedMetaInfoTimeoutTasks = [];
private readonly Dictionary<UserData, List<CharaDataMetaInfoExtendedDto>> _sharedWithYouData = [];
private readonly Dictionary<string, CharaDataExtendedUpdateDto> _updateDtos = [];
private CancellationTokenSource _applicationCts = new();
private CancellationTokenSource _charaDataCreateCts = new();
private CancellationTokenSource _connectCts = new();
private CancellationTokenSource _getAllDataCts = new();
private CancellationTokenSource _getSharedDataCts = new();
private CancellationTokenSource _uploadCts = new();
public CharaDataManager(ILogger<CharaDataManager> logger, ApiController apiController,
CharaDataFileHandler charaDataFileHandler,
MareMediator mareMediator, IpcManager ipcManager, DalamudUtilService dalamudUtilService,
FileDownloadManagerFactory fileDownloadManagerFactory,
CharaDataConfigService charaDataConfigService, CharaDataNearbyManager charaDataNearbyManager,
CharaDataCharacterHandler charaDataCharacterHandler) : base(logger, mareMediator)
{
_apiController = apiController;
_fileHandler = charaDataFileHandler;
_ipcManager = ipcManager;
_dalamudUtilService = dalamudUtilService;
_configService = charaDataConfigService;
_nearbyManager = charaDataNearbyManager;
_characterHandler = charaDataCharacterHandler;
mareMediator.Subscribe<ConnectedMessage>(this, (msg) =>
{
_connectCts?.Cancel();
_connectCts?.Dispose();
_connectCts = new();
_ownCharaData.Clear();
_metaInfoCache.Clear();
_sharedWithYouData.Clear();
_updateDtos.Clear();
Initialized = false;
MaxCreatableCharaData = msg.Connection.ServerInfo.MaxCharaData;
if (_configService.Current.DownloadMcdDataOnConnection)
{
var token = _connectCts.Token;
_ = GetAllData(token);
_ = GetAllSharedData(token);
}
});
mareMediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
_ownCharaData.Clear();
_metaInfoCache.Clear();
_sharedWithYouData.Clear();
_updateDtos.Clear();
Initialized = false;
});
}
public Task? AttachingPoseTask { get; private set; }
public Task? CharaUpdateTask { get; set; }
public string DataApplicationProgress { get; private set; } = string.Empty;
public Task? DataApplicationTask { get; private set; }
public Task<(string Output, bool Success)>? DataCreationTask { get; private set; }
public Task? DataGetTimeoutTask { get; private set; }
public Task<(string Result, bool Success)>? DownloadMetaInfoTask { get; private set; }
public Task<List<CharaDataFullExtendedDto>>? GetAllDataTask { get; private set; }
public Task<List<CharaDataMetaInfoDto>>? GetSharedWithYouTask { get; private set; }
public Task? GetSharedWithYouTimeoutTask { get; private set; }
public IEnumerable<HandledCharaDataEntry> HandledCharaData => _characterHandler.HandledCharaData;
public bool Initialized { get; private set; }
public CharaDataMetaInfoExtendedDto? LastDownloadedMetaInfo { get; private set; }
public Task<(MareCharaFileHeader LoadedFile, long ExpectedLength)>? LoadedMcdfHeader { get; private set; }
public int MaxCreatableCharaData { get; private set; }
public Task? McdfApplicationTask { get; private set; }
public List<CharaDataMetaInfoExtendedDto> NearbyData => _nearbyData;
public IDictionary<string, CharaDataFullExtendedDto> OwnCharaData => _ownCharaData;
public IDictionary<UserData, List<CharaDataMetaInfoExtendedDto>> SharedWithYouData => _sharedWithYouData;
public Task? UiBlockingComputation { get; private set; }
public ValueProgress<string>? UploadProgress { get; private set; }
public Task<(string Output, bool Success)>? UploadTask { get; private set; }
public bool BrioAvailable => _ipcManager.Brio.APIAvailable;
public Task ApplyCharaData(CharaDataMetaInfoDto dataMetaInfoDto, string charaName)
{
return UiBlockingComputation = DataApplicationTask = Task.Run(async () =>
{
if (string.IsNullOrEmpty(charaName)) return;
var download = await _apiController.CharaDataDownload(dataMetaInfoDto.Uploader.UID + ":" + dataMetaInfoDto.Id).ConfigureAwait(false);
if (download == null)
{
DataApplicationTask = null;
return;
}
await DownloadAndAplyDataAsync(charaName, download, dataMetaInfoDto, false).ConfigureAwait(false);
});
}
public Task ApplyCharaDataToGposeTarget(CharaDataMetaInfoDto dataMetaInfoDto)
{
return UiBlockingComputation = DataApplicationTask = Task.Run(async () =>
{
var charaName = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GposeTargetGameObject?.Name.TextValue).ConfigureAwait(false)
?? string.Empty;
if (string.IsNullOrEmpty(charaName)) return;
await ApplyCharaData(dataMetaInfoDto, charaName).ConfigureAwait(false);
});
}
public void ApplyOwnDataToGposeTarget(CharaDataFullExtendedDto dataDto)
{
var charaName = _dalamudUtilService.GposeTargetGameObject?.Name.TextValue ?? string.Empty;
CharaDataDownloadDto downloadDto = new(dataDto.Id, dataDto.Uploader)
{
CustomizeData = dataDto.CustomizeData,
Description = dataDto.Description,
FileGamePaths = dataDto.FileGamePaths,
GlamourerData = dataDto.GlamourerData,
FileSwaps = dataDto.FileSwaps,
ManipulationData = dataDto.ManipulationData,
UpdatedDate = dataDto.UpdatedDate
};
CharaDataMetaInfoDto metaInfoDto = new(dataDto.Id, dataDto.Uploader)
{
CanBeDownloaded = true,
Description = dataDto.Description,
PoseData = dataDto.PoseData,
UpdatedDate = dataDto.UpdatedDate,
};
UiBlockingComputation = DataApplicationTask = DownloadAndAplyDataAsync(charaName, downloadDto, metaInfoDto, false);
}
public Task ApplyPoseData(PoseEntry pose, string targetName)
{
return UiBlockingComputation = Task.Run(async () =>
{
if (string.IsNullOrEmpty(pose.PoseData) || !CanApplyInGpose(out _)) return;
var gposeChara = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(targetName, true).ConfigureAwait(false);
if (gposeChara == null) return;
var poseJson = Encoding.UTF8.GetString(LZ4Wrapper.Unwrap(Convert.FromBase64String(pose.PoseData)));
if (string.IsNullOrEmpty(poseJson)) return;
await _ipcManager.Brio.SetPoseAsync(gposeChara.Address, poseJson).ConfigureAwait(false);
});
}
public Task ApplyPoseDataToGPoseTarget(PoseEntry pose)
{
return UiBlockingComputation = Task.Run(async () =>
{
if (CanApplyInGpose(out var chara))
{
await ApplyPoseData(pose, chara).ConfigureAwait(false);
}
});
}
public Task ApplyWorldDataToTarget(PoseEntry pose, string targetName)
{
return UiBlockingComputation = Task.Run(async () =>
{
if (pose.WorldData == default || !CanApplyInGpose(out _)) return;
var gposeChara = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(targetName, true).ConfigureAwait(false);
if (gposeChara == null) return;
if (pose.WorldData == null || pose.WorldData == default) return;
Logger.LogDebug("Applying World data {data}", pose.WorldData);
await _ipcManager.Brio.ApplyTransformAsync(gposeChara.Address, pose.WorldData.Value).ConfigureAwait(false);
});
}
public Task ApplyWorldDataToGPoseTarget(PoseEntry pose)
{
return UiBlockingComputation = Task.Run(async () =>
{
if (CanApplyInGpose(out var chara))
{
await ApplyPoseData(pose, chara).ConfigureAwait(false);
}
});
}
public void AttachWorldData(PoseEntry pose, CharaDataExtendedUpdateDto updateDto)
{
AttachingPoseTask = Task.Run(async () =>
{
ICharacter? playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
if (playerChar == null) return;
if (_dalamudUtilService.IsInGpose)
{
playerChar = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(playerChar.Name.TextValue, true).ConfigureAwait(false);
}
if (playerChar == null) return;
var worldData = await _ipcManager.Brio.GetTransformAsync(playerChar.Address).ConfigureAwait(false);
if (worldData == default) return;
Logger.LogTrace("Attaching World data {data}", worldData);
worldData.LocationInfo = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
Logger.LogTrace("World data serialized: {data}", worldData);
pose.WorldData = worldData;
updateDto.UpdatePoseList();
});
}
public bool CanApplyInGpose(out string targetName)
{
bool canApply = _dalamudUtilService.IsInGpose && _dalamudUtilService.GposeTargetGameObject != null
&& _dalamudUtilService.GposeTargetGameObject.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player;
if (canApply)
{
targetName = _dalamudUtilService.GposeTargetGameObject!.Name.TextValue;
}
else
{
targetName = "Invalid Target";
}
return canApply;
}
public void CancelDataApplication()
{
_applicationCts.Cancel();
}
public void CancelUpload()
{
_uploadCts.Cancel();
}
public void CreateCharaDataEntry(CancellationToken cancelToken)
{
UiBlockingComputation = DataCreationTask = Task.Run(async () =>
{
var result = await _apiController.CharaDataCreate().ConfigureAwait(false);
_ = Task.Run(async () =>
{
_charaDataCreateCts = _charaDataCreateCts.CancelRecreate();
using var ct = CancellationTokenSource.CreateLinkedTokenSource(_charaDataCreateCts.Token, cancelToken);
await Task.Delay(TimeSpan.FromSeconds(10), ct.Token).ConfigureAwait(false);
DataCreationTask = null;
});
if (result == null)
return ("Failed to create character data, see log for more information", false);
await AddOrUpdateDto(result).ConfigureAwait(false);
return ("Created Character Data", true);
});
}
public async Task DeleteCharaData(CharaDataFullExtendedDto dto)
{
var ret = await _apiController.CharaDataDelete(dto.Id).ConfigureAwait(false);
if (ret)
{
_ownCharaData.Remove(dto.Id);
_metaInfoCache.Remove(dto.FullId);
}
DistributeMetaInfo();
}
public void DownloadMetaInfo(string importCode, bool store = true)
{
DownloadMetaInfoTask = Task.Run(async () =>
{
try
{
if (store)
{
LastDownloadedMetaInfo = null;
}
var metaInfo = await _apiController.CharaDataGetMetainfo(importCode).ConfigureAwait(false);
_sharedMetaInfoTimeoutTasks[importCode] = Task.Delay(TimeSpan.FromSeconds(10));
if (metaInfo == null)
{
_metaInfoCache[importCode] = null;
return ("Failed to download meta info for this code. Check if the code is valid and you have rights to access it.", false);
}
await CacheData(metaInfo).ConfigureAwait(false);
if (store)
{
LastDownloadedMetaInfo = await CharaDataMetaInfoExtendedDto.Create(metaInfo, _dalamudUtilService).ConfigureAwait(false);
}
return ("Ok", true);
}
finally
{
if (!store)
DownloadMetaInfoTask = null;
}
});
}
public async Task GetAllData(CancellationToken cancelToken)
{
foreach (var data in _ownCharaData)
{
_metaInfoCache.Remove(data.Key);
}
_ownCharaData.Clear();
UiBlockingComputation = GetAllDataTask = Task.Run(async () =>
{
_getAllDataCts = _getAllDataCts.CancelRecreate();
var result = await _apiController.CharaDataGetOwn().ConfigureAwait(false);
Initialized = true;
if (result.Any())
{
DataGetTimeoutTask = Task.Run(async () =>
{
using var ct = CancellationTokenSource.CreateLinkedTokenSource(_getAllDataCts.Token, cancelToken);
#if !DEBUG
await Task.Delay(TimeSpan.FromMinutes(1), ct.Token).ConfigureAwait(false);
#else
await Task.Delay(TimeSpan.FromSeconds(5), ct.Token).ConfigureAwait(false);
#endif
});
}
return result.OrderBy(u => u.CreatedDate).Select(k => new CharaDataFullExtendedDto(k)).ToList();
});
var result = await GetAllDataTask.ConfigureAwait(false);
foreach (var item in result)
{
await AddOrUpdateDto(item).ConfigureAwait(false);
}
foreach (var id in _updateDtos.Keys.Where(r => !result.Exists(res => string.Equals(res.Id, r, StringComparison.Ordinal))).ToList())
{
_updateDtos.Remove(id);
}
GetAllDataTask = null;
}
public async Task GetAllSharedData(CancellationToken token)
{
Logger.LogDebug("Getting Shared with You Data");
UiBlockingComputation = GetSharedWithYouTask = _apiController.CharaDataGetShared();
_sharedWithYouData.Clear();
GetSharedWithYouTimeoutTask = Task.Run(async () =>
{
_getSharedDataCts = _getSharedDataCts.CancelRecreate();
using var ct = CancellationTokenSource.CreateLinkedTokenSource(_getSharedDataCts.Token, token);
#if !DEBUG
await Task.Delay(TimeSpan.FromMinutes(1), ct.Token).ConfigureAwait(false);
#else
await Task.Delay(TimeSpan.FromSeconds(5), ct.Token).ConfigureAwait(false);
#endif
GetSharedWithYouTimeoutTask = null;
Logger.LogDebug("Finished Shared with You Data Timeout");
});
var result = await GetSharedWithYouTask.ConfigureAwait(false);
foreach (var grouping in result.GroupBy(r => r.Uploader))
{
List<CharaDataMetaInfoExtendedDto> newList = new();
foreach (var item in grouping)
{
var extended = await CharaDataMetaInfoExtendedDto.Create(item, _dalamudUtilService).ConfigureAwait(false);
newList.Add(extended);
CacheData(extended);
}
_sharedWithYouData[grouping.Key] = newList;
}
DistributeMetaInfo();
Logger.LogDebug("Finished getting Shared with You Data");
GetSharedWithYouTask = null;
}
public CharaDataExtendedUpdateDto? GetUpdateDto(string id)
{
if (_updateDtos.TryGetValue(id, out var dto))
return dto;
return null;
}
public bool IsInTimeout(string key)
{
if (!_sharedMetaInfoTimeoutTasks.TryGetValue(key, out var task)) return false;
return !task?.IsCompleted ?? false;
}
public void LoadMcdf(string filePath)
{
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
}
public void McdfApplyToGposeTarget()
{
if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;
var charaName = _dalamudUtilService.GposeTargetGameObject?.Name.TextValue ?? string.Empty;
List<string> actuallyExtractedFiles = [];
UiBlockingComputation = McdfApplicationTask = Task.Run(async () =>
{
Guid applicationId = Guid.NewGuid();
try
{
using GameObjectHandler? tempHandler = await _characterHandler.TryCreateGameObjectHandler(charaName, true).ConfigureAwait(false);
if (tempHandler == null) return;
var playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
bool isSelf = playerChar != null && string.Equals(playerChar.Name.TextValue, tempHandler.Name, StringComparison.Ordinal);
long expectedExtractedSize = LoadedMcdfHeader.Result.ExpectedLength;
var charaFile = LoadedMcdfHeader.Result.LoadedFile;
DataApplicationProgress = "Extracting MCDF data";
var extractedFiles = _fileHandler.McdfExtractFiles(charaFile, expectedExtractedSize, actuallyExtractedFiles);
foreach (var entry in charaFile.CharaFileData.FileSwaps.SelectMany(k => k.GamePaths, (k, p) => new KeyValuePair<string, string>(p, k.FileSwapPath)))
{
extractedFiles[entry.Key] = entry.Value;
}
DataApplicationProgress = "Applying MCDF data";
var extended = await CharaDataMetaInfoExtendedDto.Create(new(charaFile.FilePath, new UserData(string.Empty)), _dalamudUtilService)
.ConfigureAwait(false);
await ApplyDataAsync(applicationId, tempHandler, isSelf, autoRevert: false, extended,
extractedFiles, charaFile.CharaFileData.ManipulationData, charaFile.CharaFileData.GlamourerData,
charaFile.CharaFileData.CustomizePlusData, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to extract MCDF");
throw;
}
finally
{
// delete extracted files
foreach (var file in actuallyExtractedFiles)
{
File.Delete(file);
}
}
});
}
public void SaveMareCharaFile(string description, string filePath)
{
UiBlockingComputation = Task.Run(async () => await _fileHandler.SaveCharaFileAsync(description, filePath).ConfigureAwait(false));
}
public void SetAppearanceData(string dtoId)
{
var hasDto = _ownCharaData.TryGetValue(dtoId, out var dto);
if (!hasDto || dto == null) return;
var hasUpdateDto = _updateDtos.TryGetValue(dtoId, out var updateDto);
if (!hasUpdateDto || updateDto == null) return;
UiBlockingComputation = Task.Run(async () =>
{
await _fileHandler.UpdateCharaDataAsync(updateDto).ConfigureAwait(false);
});
}
public Task<HandledCharaDataEntry?> SpawnAndApplyData(CharaDataMetaInfoDto charaDataMetaInfoDto)
{
var task = Task.Run(async () =>
{
var newActor = await _ipcManager.Brio.SpawnActorAsync().ConfigureAwait(false);
if (newActor == null) return null;
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
await ApplyCharaData(charaDataMetaInfoDto, newActor.Name.TextValue).ConfigureAwait(false);
return _characterHandler.HandledCharaData.FirstOrDefault(f => string.Equals(f.Name, newActor.Name.TextValue, StringComparison.Ordinal));
});
UiBlockingComputation = task;
return task;
}
private async Task<CharaDataMetaInfoExtendedDto> CacheData(CharaDataFullExtendedDto ownCharaData)
{
var metaInfo = new CharaDataMetaInfoDto(ownCharaData.Id, ownCharaData.Uploader)
{
Description = ownCharaData.Description,
UpdatedDate = ownCharaData.UpdatedDate,
CanBeDownloaded = !string.IsNullOrEmpty(ownCharaData.GlamourerData) && (ownCharaData.OriginalFiles.Count == ownCharaData.FileGamePaths.Count),
PoseData = ownCharaData.PoseData,
};
var extended = await CharaDataMetaInfoExtendedDto.Create(metaInfo, _dalamudUtilService, isOwnData: true).ConfigureAwait(false);
_metaInfoCache[extended.FullId] = extended;
DistributeMetaInfo();
return extended;
}
private async Task<CharaDataMetaInfoExtendedDto> CacheData(CharaDataMetaInfoDto metaInfo, bool isOwnData = false)
{
var extended = await CharaDataMetaInfoExtendedDto.Create(metaInfo, _dalamudUtilService, isOwnData).ConfigureAwait(false);
_metaInfoCache[extended.FullId] = extended;
DistributeMetaInfo();
return extended;
}
private void DistributeMetaInfo()
{
_nearbyManager.UpdateSharedData(_metaInfoCache);
_characterHandler.UpdateHandledData(_metaInfoCache);
}
private void CacheData(CharaDataMetaInfoExtendedDto charaData)
{
_metaInfoCache[charaData.FullId] = charaData;
}
public bool TryGetMetaInfo(string key, out CharaDataMetaInfoExtendedDto? metaInfo)
{
return _metaInfoCache.TryGetValue(key, out metaInfo);
}
public void UploadCharaData(string id)
{
var hasUpdateDto = _updateDtos.TryGetValue(id, out var updateDto);
if (!hasUpdateDto || updateDto == null) return;
UiBlockingComputation = CharaUpdateTask = CharaUpdateAsync(updateDto);
}
public void UploadMissingFiles(string id)
{
var hasDto = _ownCharaData.TryGetValue(id, out var dto);
if (!hasDto || dto == null) return;
var missingFileList = dto.MissingFiles.ToList();
UiBlockingComputation = UploadTask = UploadFiles(missingFileList, async () =>
{
var newFilePaths = dto.FileGamePaths;
foreach (var missing in missingFileList)
{
newFilePaths.Add(missing);
}
CharaDataUpdateDto updateDto = new(dto.Id)
{
FileGamePaths = newFilePaths
};
var res = await _apiController.CharaDataUpdate(updateDto).ConfigureAwait(false);
await AddOrUpdateDto(res).ConfigureAwait(false);
});
}
internal void ApplyDataToSelf(CharaDataFullExtendedDto dataDto)
{
var chara = _dalamudUtilService.GetPlayerName();
CharaDataDownloadDto downloadDto = new(dataDto.Id, dataDto.Uploader)
{
CustomizeData = dataDto.CustomizeData,
Description = dataDto.Description,
FileGamePaths = dataDto.FileGamePaths,
GlamourerData = dataDto.GlamourerData,
FileSwaps = dataDto.FileSwaps,
ManipulationData = dataDto.ManipulationData,
UpdatedDate = dataDto.UpdatedDate
};
CharaDataMetaInfoDto metaInfoDto = new(dataDto.Id, dataDto.Uploader)
{
CanBeDownloaded = true,
Description = dataDto.Description,
PoseData = dataDto.PoseData,
UpdatedDate = dataDto.UpdatedDate,
};
UiBlockingComputation = DataApplicationTask = DownloadAndAplyDataAsync(chara, downloadDto, metaInfoDto);
}
internal void AttachPoseData(PoseEntry pose, CharaDataExtendedUpdateDto updateDto)
{
AttachingPoseTask = Task.Run(async () =>
{
ICharacter? playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
if (playerChar == null) return;
if (_dalamudUtilService.IsInGpose)
{
playerChar = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(playerChar.Name.TextValue, true).ConfigureAwait(false);
}
if (playerChar == null) return;
var poseData = await _ipcManager.Brio.GetPoseAsync(playerChar.Address).ConfigureAwait(false);
if (poseData == null) return;
var compressedByteData = LZ4Wrapper.WrapHC(Encoding.UTF8.GetBytes(poseData));
pose.PoseData = Convert.ToBase64String(compressedByteData);
updateDto.UpdatePoseList();
});
}
internal void McdfSpawnApplyToGposeTarget()
{
UiBlockingComputation = Task.Run(async () =>
{
var newActor = await _ipcManager.Brio.SpawnActorAsync().ConfigureAwait(false);
if (newActor == null) return;
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
unsafe
{
_dalamudUtilService.GposeTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)newActor.Address;
}
McdfApplyToGposeTarget();
});
}
internal void ApplyFullPoseDataToTarget(PoseEntry value, string targetName)
{
UiBlockingComputation = Task.Run(async () =>
{
await ApplyPoseData(value, targetName).ConfigureAwait(false);
await ApplyWorldDataToTarget(value, targetName).ConfigureAwait(false);
});
}
internal void ApplyFullPoseDataToGposeTarget(PoseEntry value)
{
UiBlockingComputation = Task.Run(async () =>
{
if (CanApplyInGpose(out var gposeTarget))
{
await ApplyPoseData(value, gposeTarget).ConfigureAwait(false);
await ApplyWorldDataToTarget(value, gposeTarget).ConfigureAwait(false);
}
});
}
internal void SpawnAndApplyWorldTransform(CharaDataMetaInfoDto metaInfo, PoseEntry value)
{
UiBlockingComputation = Task.Run(async () =>
{
var actor = await SpawnAndApplyData(metaInfo).ConfigureAwait(false);
if (actor == null) return;
await ApplyPoseData(value, actor.Name).ConfigureAwait(false);
await ApplyWorldDataToTarget(value, actor.Name).ConfigureAwait(false);
});
}
internal unsafe void TargetGposeActor(HandledCharaDataEntry actor)
{
var gposeActor = _dalamudUtilService.GetGposeCharacterFromObjectTableByName(actor.Name, true);
if (gposeActor != null)
{
_dalamudUtilService.GposeTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gposeActor.Address;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_getAllDataCts?.Cancel();
_getAllDataCts?.Dispose();
_getSharedDataCts?.Cancel();
_getSharedDataCts?.Dispose();
_charaDataCreateCts?.Cancel();
_charaDataCreateCts?.Dispose();
_uploadCts?.Cancel();
_uploadCts?.Dispose();
_applicationCts.Cancel();
_applicationCts.Dispose();
_connectCts?.Cancel();
_connectCts?.Dispose();
}
}
private async Task AddOrUpdateDto(CharaDataFullDto? dto)
{
if (dto == null) return;
_ownCharaData[dto.Id] = new(dto);
_updateDtos[dto.Id] = new(new(dto.Id), _ownCharaData[dto.Id]);
await CacheData(_ownCharaData[dto.Id]).ConfigureAwait(false);
}
private async Task ApplyDataAsync(Guid applicationId, GameObjectHandler tempHandler, bool isSelf, bool autoRevert,
CharaDataMetaInfoExtendedDto metaInfo, Dictionary<string, string> modPaths, string? manipData, string? glamourerData, string? customizeData, CancellationToken token)
{
Guid? cPlusId = null;
Guid penumbraCollection;
try
{
DataApplicationProgress = "Reverting previous Application";
Logger.LogTrace("[{appId}] Reverting chara {chara}", applicationId, tempHandler.Name);
bool reverted = await _characterHandler.RevertHandledChara(tempHandler.Name).ConfigureAwait(false);
if (reverted)
await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
Logger.LogTrace("[{appId}] Applying data in Penumbra", applicationId);
DataApplicationProgress = "Applying Penumbra information";
penumbraCollection = await _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, metaInfo.Uploader.UID + metaInfo.Id).ConfigureAwait(false);
var idx = await _dalamudUtilService.RunOnFrameworkThread(() => tempHandler.GetGameObject()?.ObjectIndex).ConfigureAwait(false) ?? 0;
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, idx).ConfigureAwait(false);
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, applicationId, penumbraCollection, modPaths).ConfigureAwait(false);
await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, applicationId, penumbraCollection, manipData ?? string.Empty).ConfigureAwait(false);
Logger.LogTrace("[{appId}] Applying Glamourer data and Redrawing", applicationId);
DataApplicationProgress = "Applying Glamourer and redrawing Character";
await _ipcManager.Glamourer.ApplyAllAsync(Logger, tempHandler, glamourerData, applicationId, token).ConfigureAwait(false);
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, token).ConfigureAwait(false);
await _dalamudUtilService.WaitWhileCharacterIsDrawing(Logger, tempHandler, applicationId, ct: token).ConfigureAwait(false);
Logger.LogTrace("[{appId}] Removing collection", applicationId);
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, penumbraCollection).ConfigureAwait(false);
DataApplicationProgress = "Applying Customize+ data";
Logger.LogTrace("[{appId}] Appplying C+ data", applicationId);
if (!string.IsNullOrEmpty(customizeData))
{
cPlusId = await _ipcManager.CustomizePlus.SetBodyScaleAsync(tempHandler.Address, customizeData).ConfigureAwait(false);
}
else
{
cPlusId = await _ipcManager.CustomizePlus.SetBodyScaleAsync(tempHandler.Address, Convert.ToBase64String(Encoding.UTF8.GetBytes("{}"))).ConfigureAwait(false);
}
if (autoRevert)
{
Logger.LogTrace("[{appId}] Starting wait for auto revert", applicationId);
int i = 15;
while (i > 0)
{
DataApplicationProgress = $"All data applied. Reverting automatically in {i} seconds.";
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
i--;
}
}
else
{
Logger.LogTrace("[{appId}] Adding {name} to handled objects", applicationId, tempHandler.Name);
_characterHandler.AddHandledChara(new HandledCharaDataEntry(tempHandler.Name, isSelf, cPlusId, metaInfo));
}
}
finally
{
if (token.IsCancellationRequested)
DataApplicationProgress = "Application aborted. Reverting Character...";
else if (autoRevert)
DataApplicationProgress = "Application finished. Reverting Character...";
if (autoRevert)
{
await _characterHandler.RevertChara(tempHandler.Name, cPlusId).ConfigureAwait(false);
}
if (!_dalamudUtilService.IsInGpose)
Mediator.Publish(new HaltCharaDataCreation(Resume: true));
if (_configService.Current.FavoriteCodes.TryGetValue(metaInfo.Uploader.UID + ":" + metaInfo.Id, out var favorite) && favorite != null)
{
favorite.LastDownloaded = DateTime.UtcNow;
_configService.Save();
}
DataApplicationTask = null;
DataApplicationProgress = string.Empty;
}
}
private async Task CharaUpdateAsync(CharaDataExtendedUpdateDto updateDto)
{
Logger.LogDebug("Uploading Chara Data to Server");
var baseUpdateDto = updateDto.BaseDto;
if (baseUpdateDto.FileGamePaths != null)
{
Logger.LogDebug("Detected file path changes, starting file upload");
UploadTask = UploadFiles(baseUpdateDto.FileGamePaths);
var result = await UploadTask.ConfigureAwait(false);
if (!result.Success)
{
return;
}
}
Logger.LogDebug("Pushing update dto to server: {data}", baseUpdateDto);
var res = await _apiController.CharaDataUpdate(baseUpdateDto).ConfigureAwait(false);
await AddOrUpdateDto(res).ConfigureAwait(false);
CharaUpdateTask = null;
}
private async Task DownloadAndAplyDataAsync(string charaName, CharaDataDownloadDto charaDataDownloadDto, CharaDataMetaInfoDto metaInfo, bool autoRevert = true)
{
_applicationCts = _applicationCts.CancelRecreate();
var token = _applicationCts.Token;
ICharacter? chara = (await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(charaName, _dalamudUtilService.IsInGpose).ConfigureAwait(false));
if (chara == null)
return;
var applicationId = Guid.NewGuid();
var playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
bool isSelf = playerChar != null && string.Equals(playerChar.Name.TextValue, chara.Name.TextValue, StringComparison.Ordinal);
DataApplicationProgress = "Checking local files";
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
Dictionary<string, string> modPaths;
List<FileReplacementData> missingFiles;
_fileHandler.ComputeMissingFiles(charaDataDownloadDto, out modPaths, out missingFiles);
Logger.LogTrace("[{appId}] Computing local missing files", applicationId);
using GameObjectHandler? tempHandler = await _characterHandler.TryCreateGameObjectHandler(chara.ObjectIndex).ConfigureAwait(false);
if (tempHandler == null) return;
if (missingFiles.Any())
{
try
{
DataApplicationProgress = "Downloading Missing Files. Please be patient.";
await _fileHandler.DownloadFilesAsync(tempHandler, missingFiles, modPaths, token).ConfigureAwait(false);
}
catch (FileNotFoundException)
{
DataApplicationProgress = "Failed to download one or more files. Aborting.";
DataApplicationTask = null;
return;
}
catch (OperationCanceledException)
{
DataApplicationProgress = "Application aborted.";
DataApplicationTask = null;
return;
}
}
if (!_dalamudUtilService.IsInGpose)
Mediator.Publish(new HaltCharaDataCreation());
var extendedMetaInfo = await CacheData(metaInfo).ConfigureAwait(false);
await ApplyDataAsync(applicationId, tempHandler, isSelf, autoRevert, extendedMetaInfo, modPaths, charaDataDownloadDto.ManipulationData, charaDataDownloadDto.GlamourerData,
charaDataDownloadDto.CustomizeData, token).ConfigureAwait(false);
}
private async Task<(string Result, bool Success)> UploadFiles(List<GamePathEntry> missingFileList, Func<Task>? postUpload = null)
{
UploadProgress = new ValueProgress<string>();
try
{
_uploadCts = _uploadCts.CancelRecreate();
var missingFiles = await _fileHandler.UploadFiles([.. missingFileList.Select(k => k.HashOrFileSwap)], UploadProgress, _uploadCts.Token).ConfigureAwait(false);
if (missingFiles.Any())
{
Logger.LogInformation("Failed to upload {files}", string.Join(", ", missingFiles));
return ($"Upload failed: {missingFiles.Count} missing or forbidden to upload local files.", false);
}
if (postUpload != null)
await postUpload.Invoke().ConfigureAwait(false);
return ("Upload sucessful", true);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during upload");
if (ex is OperationCanceledException)
{
return ("Upload Cancelled", false);
}
return ("Error in upload, see log for more details", false);
}
finally
{
UploadTask = null;
UploadProgress = null;
}
}
public void RevertChara(HandledCharaDataEntry? handled)
{
UiBlockingComputation = _characterHandler.RevertHandledChara(handled);
}
internal void RemoveChara(string handledActor)
{
if (string.IsNullOrEmpty(handledActor)) return;
UiBlockingComputation = Task.Run(async () =>
{
await _characterHandler.RevertHandledChara(handledActor, false).ConfigureAwait(false);
var gposeChara = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(handledActor, true).ConfigureAwait(false);
if (gposeChara != null)
await _ipcManager.Brio.DespawnActorAsync(gposeChara.Address).ConfigureAwait(false);
});
}
}

View File

@@ -0,0 +1,288 @@
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.Diagnostics.Eventing.Reader;
using System.Numerics;
namespace MareSynchronos.Services;
internal sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
{
internal 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;
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)
{
_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);
}
}
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)
await _dalamudUtilService.RunOnFrameworkThread(() => ManageWispsNearby(previousPoses)).ConfigureAwait(false);
}
private unsafe void HandleFrameworkUpdate()
{
if (_lastExecutionTime.AddSeconds(0.5) > DateTime.UtcNow) return;
_lastExecutionTime = DateTime.UtcNow;
if (!ComputeNearbyData)
{
if (_nearbyData.Any())
_nearbyData.Clear();
if (_poseVfx.Any())
ClearAllVfx();
return;
}
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose)
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)
_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;
internal 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,332 @@
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();
_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,
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<PoseEntry> PoseList => _poseList;
private readonly List<PoseEntry> _poseList;
public void AddToList(string user)
{
_userList.Add(new(user, null));
UpdateAllowedUsers();
}
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;
}
}
public void RemoveFromList(string user)
{
_userList.RemoveAll(u => string.Equals(u.UID, user, StringComparison.Ordinal));
UpdateAllowedUsers();
}
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;
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
|| 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,6 @@
namespace MareSynchronos.Services.CharaData.Models;
public sealed record HandledCharaDataEntry(string Name, bool IsSelf, Guid? CustomizePlus, CharaDataMetaInfoExtendedDto MetaInfo)
{
public CharaDataMetaInfoExtendedDto MetaInfo { get; set; } = MetaInfo;
}

View File

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

View File

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

View File

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

View File

@@ -107,7 +107,7 @@ public sealed class CommandManagerService : IDisposable
}
else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase))
{
_mediator.Publish(new UiToggleMessage(typeof(GposeUi)));
_mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
}
else if (string.Equals(splitArgs[0], "rescan", StringComparison.OrdinalIgnoreCase))
{

View File

@@ -1,10 +1,16 @@
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.Sheets;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.Interop;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator;
@@ -13,6 +19,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject;
@@ -83,6 +90,43 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
.Where(x => x.RowId != 0 && !(x.RowId >= 500 && (x.Dark & 0xFFFFFF00) == 0))
.ToDictionary(x => (int)x.RowId);
});
TerritoryData = new(() =>
{
return gameData.GetExcelSheet<Lumina.Excel.Sheets.TerritoryType>(Dalamud.Game.ClientLanguage.English)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
StringBuilder sb = new();
sb.Append(w.PlaceNameRegion.Value.Name);
if (w.PlaceName.ValueNullable != null)
{
sb.Append(" - ");
sb.Append(w.PlaceName.Value.Name);
}
return sb.ToString();
});
});
MapData = new(() =>
{
return gameData.GetExcelSheet<Lumina.Excel.Sheets.Map>(Dalamud.Game.ClientLanguage.English)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
StringBuilder sb = new();
sb.Append(w.PlaceNameRegion.Value.Name);
if (w.PlaceName.ValueNullable != null)
{
sb.Append(" - ");
sb.Append(w.PlaceName.Value.Name);
}
if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString()))
{
sb.Append(" - ");
sb.Append(w.PlaceNameSub.Value.Name);
}
return (w, sb.ToString());
});
});
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
{
if (clientState.IsPvP) return;
@@ -103,7 +147,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
}
public bool IsWine { get; init; }
public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget;
public unsafe GameObject* GposeTarget
{
get => TargetSystem.Instance()->GPoseTarget;
set => TargetSystem.Instance()->GPoseTarget = value;
}
public unsafe Dalamud.Game.ClientState.Objects.Types.IGameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex];
public bool IsAnythingDrawing { get; private set; } = false;
public bool IsInCutscene { get; private set; } = false;
@@ -116,6 +164,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; }
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
public Lazy<Dictionary<uint, (Lumina.Excel.Sheets.Map Map, string MapName)>> MapData { get; private set; }
public MareMediator Mediator { get; }
@@ -157,13 +207,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return await RunOnFrameworkThread(() => GetCompanion(playerPointer)).ConfigureAwait(false);
}
public Dalamud.Game.ClientState.Objects.Types.ICharacter? GetGposeCharacterFromObjectTableByName(string name, bool onlyGposeCharacters = false)
public async Task<ICharacter?> GetGposeCharacterFromObjectTableByNameAsync(string name, bool onlyGposeCharacters = false)
{
return await RunOnFrameworkThread(() => GetGposeCharacterFromObjectTableByName(name, onlyGposeCharacters)).ConfigureAwait(false);
}
public ICharacter? GetGposeCharacterFromObjectTableByName(string name, bool onlyGposeCharacters = false)
{
EnsureIsOnFramework();
return (Dalamud.Game.ClientState.Objects.Types.ICharacter?)_objectTable
return (ICharacter?)_objectTable
.FirstOrDefault(i => (!onlyGposeCharacters || i.ObjectIndex >= 200) && string.Equals(i.Name.ToString(), name, StringComparison.Ordinal));
}
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
{
return _objectTable.Where(o => o.ObjectIndex > 200 && o.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player).Cast<ICharacter>();
}
public bool GetIsPlayerPresent()
{
EnsureIsOnFramework();
@@ -203,6 +263,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return await RunOnFrameworkThread(() => GetPet(playerPointer)).ConfigureAwait(false);
}
public async Task<IPlayerCharacter> GetPlayerCharacterAsync()
{
return await RunOnFrameworkThread(GetPlayerCharacter).ConfigureAwait(false);
}
public IPlayerCharacter GetPlayerCharacter()
{
EnsureIsOnFramework();
return _clientState.LocalPlayer!;
}
public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName)
{
if (_playerCharas.TryGetValue(characterName, out var pchar)) return pchar.Address;
@@ -248,6 +319,60 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return _clientState.LocalPlayer!.CurrentWorld.RowId;
}
public unsafe LocationInfo GetMapData()
{
EnsureIsOnFramework();
var agentMap = AgentMap.Instance();
var houseMan = HousingManager.Instance();
uint serverId = 0;
if (_clientState.LocalPlayer == null) serverId = 0;
else serverId = _clientState.LocalPlayer.CurrentWorld.RowId;
uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId;
uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId;
uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision());
uint wardId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentWard() + 1);
uint houseId = 0;
var tempHouseId = houseMan == null ? 0 : (houseMan->GetCurrentPlot());
if (!houseMan->IsInside()) tempHouseId = 0;
if (tempHouseId < -1)
{
divisionId = tempHouseId == -127 ? 2 : (uint)1;
tempHouseId = 100;
}
if (tempHouseId == -1) tempHouseId = 0;
houseId = (uint)tempHouseId;
if (houseId != 0)
{
territoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
}
uint roomId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentRoom());
return new LocationInfo()
{
ServerId = serverId,
MapId = mapId,
TerritoryId = territoryId,
DivisionId = divisionId,
WardId = wardId,
HouseId = houseId,
RoomId = roomId
};
}
public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
{
EnsureIsOnFramework();
var agentMap = AgentMap.Instance();
if (agentMap == null) return;
agentMap->OpenMapByMapId(map.RowId);
agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position);
}
public async Task<LocationInfo> GetMapDataAsync()
{
return await RunOnFrameworkThread(GetMapData).ConfigureAwait(false);
}
public async Task<uint> GetWorldIdAsync()
{
return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false);
@@ -274,7 +399,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false);
}
public async Task RunOnFrameworkThread(Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
public async Task RunOnFrameworkThread(System.Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
{
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
await _performanceCollector.LogPerformance(this, $"RunOnFramework:Act/{fileName}>{callerMember}:{callerLineNumber}", async () =>
@@ -534,13 +659,13 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_lastGlobalBlockReason = string.Empty;
}
if (GposeTarget != null && !IsInGpose)
if (_clientState.IsGPosing && !IsInGpose)
{
_logger.LogDebug("Gpose start");
IsInGpose = true;
Mediator.Publish(new GposeStartMessage());
}
else if (GposeTarget == null && IsInGpose)
else if (!_clientState.IsGPosing && IsInGpose)
{
_logger.LogDebug("Gpose end");
IsInGpose = false;

View File

@@ -25,7 +25,7 @@ public record DelayedFrameworkUpdateMessage : SameThreadMessage;
public record ZoneSwitchStartMessage : MessageBase;
public record ZoneSwitchEndMessage : MessageBase;
public record CutsceneStartMessage : MessageBase;
public record GposeStartMessage : MessageBase;
public record GposeStartMessage : SameThreadMessage;
public record GposeEndMessage : MessageBase;
public record CutsceneEndMessage : MessageBase;
public record CutsceneFrameworkUpdateMessage : SameThreadMessage;
@@ -99,6 +99,7 @@ public record PairDataAppliedMessage(string UID, CharacterData? CharacterData) :
public record PairDataAnalyzedMessage(string UID) : KeyedMessage(UID);
public record GameObjectHandlerCreatedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : MessageBase;
public record GameObjectHandlerDestroyedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : MessageBase;
public record HaltCharaDataCreation(bool Resume = false) : SameThreadMessage;
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
#pragma warning restore S2094