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:
141
MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs
Normal file
141
MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
937
MareSynchronos/Services/CharaData/CharaDataManager.cs
Normal file
937
MareSynchronos/Services/CharaData/CharaDataManager.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
288
MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
Normal file
288
MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user