Add MCDO (#80)

* update api

* mcd online editor impl

* most of chara data hub impl

* some state of things

* some refactoring

* random bullshit go

* more nearby impl

* add uid to peformance msg

* cleanup/homogenization

* some split, update nuget packages

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

* some polish and cleanup

* more cleanup, beautification, etc.

* fixes and cleanups

---------

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

Submodule MareAPI updated: 9c9b7d90c1...c8cc217d66

View File

@@ -112,3 +112,6 @@ dotnet_diagnostic.S6667.severity = suggestion
# IDE0290: Use primary constructor # IDE0290: Use primary constructor
csharp_style_prefer_primary_constructors = false csharp_style_prefer_primary_constructors = false
# S3267: Loops should be simplified with "LINQ" expressions
dotnet_diagnostic.S3267.severity = silent

View File

@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>", Scope = "member", Target = "~M:MareSynchronos.Services.CharaDataManager.AttachPoseData(MareSynchronos.API.Dto.CharaData.PoseEntry,MareSynchronos.Services.CharaData.Models.CharaDataExtendedUpdateDto)")]

View File

@@ -0,0 +1,146 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.Services;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Text.Json.Nodes;
namespace MareSynchronos.Interop.Ipc;
public sealed class IpcCallerBrio : IIpcCaller
{
private readonly ILogger<IpcCallerBrio> _logger;
private readonly DalamudUtilService _dalamudUtilService;
private readonly ICallGateSubscriber<(int, int)> _brioApiVersion;
private readonly ICallGateSubscriber<bool, bool, bool, Task<IGameObject>> _brioSpawnActorAsync;
private readonly ICallGateSubscriber<IGameObject, bool> _brioDespawnActor;
private readonly ICallGateSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool> _brioSetModelTransform;
private readonly ICallGateSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)> _brioGetModelTransform;
private readonly ICallGateSubscriber<IGameObject, string> _brioGetPoseAsJson;
private readonly ICallGateSubscriber<IGameObject, string, bool, bool> _brioSetPoseFromJson;
private readonly ICallGateSubscriber<IGameObject, bool> _brioFreezeActor;
private readonly ICallGateSubscriber<bool> _brioFreezePhysics;
public bool APIAvailable { get; private set; }
public IpcCallerBrio(ILogger<IpcCallerBrio> logger, IDalamudPluginInterface dalamudPluginInterface,
DalamudUtilService dalamudUtilService)
{
_logger = logger;
_dalamudUtilService = dalamudUtilService;
_brioApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("Brio.ApiVersion");
_brioSpawnActorAsync = dalamudPluginInterface.GetIpcSubscriber<bool, bool, bool, Task<IGameObject>>("Brio.Actor.SpawnExAsync");
_brioDespawnActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Despawn");
_brioSetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool>("Brio.Actor.SetModelTransform");
_brioGetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)>("Brio.Actor.GetModelTransform");
_brioGetPoseAsJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string>("Brio.Actor.Pose.GetPoseAsJson");
_brioSetPoseFromJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string, bool, bool>("Brio.Actor.Pose.LoadFromJson");
_brioFreezeActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Freeze");
_brioFreezePhysics = dalamudPluginInterface.GetIpcSubscriber<bool>("Brio.FreezePhysics");
CheckAPI();
}
public void CheckAPI()
{
try
{
var version = _brioApiVersion.InvokeFunc();
APIAvailable = (version.Item1 == 2 && version.Item2 >= 0);
}
catch
{
APIAvailable = false;
}
}
public async Task<IGameObject?> SpawnActorAsync()
{
if (!APIAvailable) return null;
_logger.LogDebug("Spawning Brio Actor");
return await _brioSpawnActorAsync.InvokeFunc(false, false, true).ConfigureAwait(false);
}
public async Task<bool> DespawnActorAsync(nint address)
{
if (!APIAvailable) return false;
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
if (gameObject == null) return false;
_logger.LogDebug("Despawning Brio Actor {actor}", gameObject.Name.TextValue);
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioDespawnActor.InvokeFunc(gameObject)).ConfigureAwait(false);
}
public async Task<bool> ApplyTransformAsync(nint address, WorldData data)
{
if (!APIAvailable) return false;
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
if (gameObject == null) return false;
_logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue);
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetModelTransform.InvokeFunc(gameObject,
new Vector3(data.PositionX, data.PositionY, data.PositionZ),
new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW),
new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false)).ConfigureAwait(false);
}
public async Task<WorldData> GetTransformAsync(nint address)
{
if (!APIAvailable) return default;
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
if (gameObject == null) return default;
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue);
return new WorldData()
{
PositionX = data.Item1.Value.X,
PositionY = data.Item1.Value.Y,
PositionZ = data.Item1.Value.Z,
RotationX = data.Item2.Value.X,
RotationY = data.Item2.Value.Y,
RotationZ = data.Item2.Value.Z,
RotationW = data.Item2.Value.W,
ScaleX = data.Item3.Value.X,
ScaleY = data.Item3.Value.Y,
ScaleZ = data.Item3.Value.Z
};
}
public async Task<string?> GetPoseAsync(nint address)
{
if (!APIAvailable) return null;
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
if (gameObject == null) return null;
_logger.LogDebug("Getting Pose from Actor {actor}", gameObject.Name.TextValue);
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
}
public async Task<bool> SetPoseAsync(nint address, string pose)
{
if (!APIAvailable) return false;
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
if (gameObject == null) return false;
_logger.LogDebug("Setting Pose to Actor {actor}", gameObject.Name.TextValue);
var applicablePose = JsonNode.Parse(pose)!;
var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
applicablePose["ModelDifference"] = JsonNode.Parse(JsonNode.Parse(currentPose)!["ModelDifference"]!.ToJsonString());
await _dalamudUtilService.RunOnFrameworkThread(() =>
{
_brioFreezeActor.InvokeFunc(gameObject);
_brioFreezePhysics.InvokeFunc();
}).ConfigureAwait(false);
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetPoseFromJson.InvokeFunc(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false);
}
public void Dispose()
{
}
}

View File

@@ -7,15 +7,16 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
{ {
public IpcManager(ILogger<IpcManager> logger, MareMediator mediator, public IpcManager(ILogger<IpcManager> logger, MareMediator mediator,
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc, IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
IpcCallerHonorific honorificIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerMoodles moodlesIpc) : base(logger, mediator) IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
{ {
CustomizePlus = customizeIpc; CustomizePlus = customizeIpc;
Heels = heelsIpc; Heels = heelsIpc;
Glamourer = glamourerIpc; Glamourer = glamourerIpc;
Penumbra = penumbraIpc; Penumbra = penumbraIpc;
Honorific = honorificIpc; Honorific = honorificIpc;
PetNames = ipcCallerPetNames;
Moodles = moodlesIpc; Moodles = moodlesIpc;
PetNames = ipcCallerPetNames;
Brio = ipcCallerBrio;
if (Initialized) if (Initialized)
{ {
@@ -41,15 +42,17 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
public IpcCallerHeels Heels { get; init; } public IpcCallerHeels Heels { get; init; }
public IpcCallerGlamourer Glamourer { get; } public IpcCallerGlamourer Glamourer { get; }
public IpcCallerPenumbra Penumbra { get; } public IpcCallerPenumbra Penumbra { get; }
public IpcCallerPetNames PetNames { get; }
public IpcCallerMoodles Moodles { get; } public IpcCallerMoodles Moodles { get; }
public IpcCallerPetNames PetNames { get; }
public IpcCallerBrio Brio { get; }
private int _stateCheckCounter = -1; private int _stateCheckCounter = -1;
private void PeriodicApiStateCheck() private void PeriodicApiStateCheck()
{ {
// Stagger API checks // Stagger API checks
if (++_stateCheckCounter > 7) if (++_stateCheckCounter > 8)
_stateCheckCounter = 0; _stateCheckCounter = 0;
int i = _stateCheckCounter; int i = _stateCheckCounter;
if (i == 0) Penumbra.CheckAPI(); if (i == 0) Penumbra.CheckAPI();
@@ -58,7 +61,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
if (i == 3) Heels.CheckAPI(); if (i == 3) Heels.CheckAPI();
if (i == 4) CustomizePlus.CheckAPI(); if (i == 4) CustomizePlus.CheckAPI();
if (i == 5) Honorific.CheckAPI(); if (i == 5) Honorific.CheckAPI();
if (i == 6) PetNames.CheckAPI(); if (i == 6) Moodles.CheckAPI();
if (i == 7) Moodles.CheckAPI(); if (i == 7) PetNames.CheckAPI();
if (i == 8) Brio.CheckAPI();
} }
} }

View File

@@ -2,7 +2,6 @@
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Export;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
@@ -16,7 +15,6 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
private readonly ILogger<IpcProvider> _logger; private readonly ILogger<IpcProvider> _logger;
private readonly IDalamudPluginInterface _pi; private readonly IDalamudPluginInterface _pi;
private readonly MareConfigService _mareConfig; private readonly MareConfigService _mareConfig;
private readonly MareCharaFileManager _mareCharaFileManager;
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private ICallGateProvider<string, IGameObject, bool>? _loadFileProvider; private ICallGateProvider<string, IGameObject, bool>? _loadFileProvider;
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProvider; private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProvider;
@@ -36,16 +34,17 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
public MareMediator Mediator { get; init; } public MareMediator Mediator { get; init; }
public IpcProvider(ILogger<IpcProvider> logger, IDalamudPluginInterface pi, MareConfigService mareConfig, public IpcProvider(ILogger<IpcProvider> logger, IDalamudPluginInterface pi, MareConfigService mareConfig,
MareCharaFileManager mareCharaFileManager, DalamudUtilService dalamudUtil, DalamudUtilService dalamudUtil,
MareMediator mareMediator) MareMediator mareMediator)
{ {
_logger = logger; _logger = logger;
_pi = pi; _pi = pi;
_mareConfig = mareConfig; _mareConfig = mareConfig;
_mareCharaFileManager = mareCharaFileManager;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
Mediator = mareMediator; Mediator = mareMediator;
// todo: fix ipc to use CharaDataManager
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) => Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
{ {
if (msg.OwnedObject) return; if (msg.OwnedObject) return;
@@ -136,7 +135,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
private async Task<bool> LoadMcdfAsync(string path, IGameObject target) private async Task<bool> LoadMcdfAsync(string path, IGameObject target)
{ {
if (_mareCharaFileManager.CurrentlyWorking || !_dalamudUtil.IsInGpose) //if (_mareCharaFileManager.CurrentlyWorking || !_dalamudUtil.IsInGpose)
return false; return false;
await ApplyFileAsync(path, target).ConfigureAwait(false); await ApplyFileAsync(path, target).ConfigureAwait(false);
@@ -146,7 +145,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
private bool LoadMcdf(string path, IGameObject target) private bool LoadMcdf(string path, IGameObject target)
{ {
if (_mareCharaFileManager.CurrentlyWorking || !_dalamudUtil.IsInGpose) //if (_mareCharaFileManager.CurrentlyWorking || !_dalamudUtil.IsInGpose)
return false; return false;
_ = Task.Run(async () => await ApplyFileAsync(path, target).ConfigureAwait(false)).ConfigureAwait(false); _ = Task.Run(async () => await ApplyFileAsync(path, target).ConfigureAwait(false)).ConfigureAwait(false);
@@ -156,6 +155,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
private async Task ApplyFileAsync(string path, IGameObject target) private async Task ApplyFileAsync(string path, IGameObject target)
{ {
/*
try try
{ {
var expectedLength = _mareCharaFileManager.LoadMareCharaFile(path); var expectedLength = _mareCharaFileManager.LoadMareCharaFile(path);
@@ -168,7 +168,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
finally finally
{ {
_mareCharaFileManager.ClearMareCharaFile(); _mareCharaFileManager.ClearMareCharaFile();
} }*/
} }
private List<nint> GetHandledAddresses() private List<nint> GetHandledAddresses()

View File

@@ -0,0 +1,179 @@
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
namespace MareSynchronos.Interop;
/// <summary>
/// Code for spawning mostly taken from https://git.anna.lgbt/anna/OrangeGuidanceTomestone/src/branch/main/client/Vfx.cs
/// </summary>
public unsafe class VfxSpawnManager : DisposableMediatorSubscriberBase
{
private static readonly byte[] _pool = "Client.System.Scheduler.Instance.VfxObject\0"u8.ToArray();
[Signature("E8 ?? ?? ?? ?? F3 0F 10 35 ?? ?? ?? ?? 48 89 43 08")]
private readonly delegate* unmanaged<byte*, byte*, VfxStruct*> _staticVfxCreate;
[Signature("E8 ?? ?? ?? ?? 8B 4B 7C 85 C9")]
private readonly delegate* unmanaged<VfxStruct*, float, int, ulong> _staticVfxRun;
[Signature("40 53 48 83 EC 20 48 8B D9 48 8B 89 ?? ?? ?? ?? 48 85 C9 74 28 33 D2 E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9")]
private readonly delegate* unmanaged<VfxStruct*, nint> _staticVfxRemove;
public VfxSpawnManager(ILogger<VfxSpawnManager> logger, IGameInteropProvider gameInteropProvider, MareMediator mareMediator)
: base(logger, mareMediator)
{
gameInteropProvider.InitializeFromAttributes(this);
mareMediator.Subscribe<GposeStartMessage>(this, (msg) =>
{
ChangeSpawnVisibility(0f);
});
mareMediator.Subscribe<GposeEndMessage>(this, (msg) =>
{
ChangeSpawnVisibility(0.5f);
});
mareMediator.Subscribe<CutsceneStartMessage>(this, (msg) =>
{
ChangeSpawnVisibility(0f);
});
mareMediator.Subscribe<CutsceneEndMessage>(this, (msg) =>
{
ChangeSpawnVisibility(0.5f);
});
}
private unsafe void ChangeSpawnVisibility(float visibility)
{
foreach (var vfx in _spawnedObjects)
{
((VfxStruct*)vfx.Value)->Alpha = visibility;
}
}
private readonly Dictionary<Guid, nint> _spawnedObjects = [];
private VfxStruct* SpawnStatic(string path, Vector3 pos, Quaternion rotation, float r, float g, float b, float a, Vector3 scale)
{
VfxStruct* vfx;
fixed (byte* terminatedPath = Encoding.UTF8.GetBytes(path).NullTerminate())
{
fixed (byte* pool = _pool)
{
vfx = _staticVfxCreate(terminatedPath, pool);
}
}
if (vfx == null)
{
return null;
}
vfx->Position = new Vector3(pos.X, pos.Y + 1, pos.Z);
vfx->Rotation = new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
vfx->SomeFlags &= 0xF7;
vfx->Flags |= 2;
vfx->Red = r;
vfx->Green = g;
vfx->Blue = b;
vfx->Scale = scale;
vfx->Alpha = a;
_staticVfxRun(vfx, 0.0f, -1);
return vfx;
}
public Guid? SpawnObject(Vector3 position, Quaternion rotation, Vector3 scale, float r = 1f, float g = 1f, float b = 1f, float a = 0.5f)
{
Logger.LogDebug("Trying to Spawn orb VFX at {pos}, {rot}", position, rotation);
var vfx = SpawnStatic("bgcommon/world/common/vfx_for_event/eff/b0150_eext_y.avfx", position, rotation, r, g, b, a, scale);
if (vfx == null || (nint)vfx == nint.Zero)
{
Logger.LogDebug("Failed to Spawn VFX at {pos}, {rot}", position, rotation);
return null;
}
Guid guid = Guid.NewGuid();
Logger.LogDebug("Spawned VFX at {pos}, {rot}: 0x{ptr:X}", position, rotation, (nint)vfx);
_spawnedObjects[guid] = (nint)vfx;
return guid;
}
public void DespawnObject(Guid id)
{
if (_spawnedObjects.Remove<Guid, nint>(id, out var vfx))
{
Logger.LogDebug("Despawning {obj:X}", vfx);
_staticVfxRemove((VfxStruct*)vfx);
}
}
private void RemoveAllVfx()
{
foreach (var obj in _spawnedObjects.Values)
{
Logger.LogDebug("Despawning {obj:X}", obj);
_staticVfxRemove((VfxStruct*)obj);
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
RemoveAllVfx();
}
}
[StructLayout(LayoutKind.Explicit)]
internal struct VfxStruct
{
[FieldOffset(0x38)]
public byte Flags;
[FieldOffset(0x50)]
public Vector3 Position;
[FieldOffset(0x60)]
public Quaternion Rotation;
[FieldOffset(0x70)]
public Vector3 Scale;
[FieldOffset(0x128)]
public int ActorCaster;
[FieldOffset(0x130)]
public int ActorTarget;
[FieldOffset(0x1B8)]
public int StaticCaster;
[FieldOffset(0x1C0)]
public int StaticTarget;
[FieldOffset(0x248)]
public byte SomeFlags;
[FieldOffset(0x260)]
public float Red;
[FieldOffset(0x264)]
public float Green;
[FieldOffset(0x268)]
public float Blue;
[FieldOffset(0x26C)]
public float Alpha;
}
}

View File

@@ -0,0 +1,11 @@
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class CharaDataConfigService : ConfigurationServiceBase<CharaDataConfig>
{
public const string ConfigName = "charadata.json";
public CharaDataConfigService(string configDir) : base(configDir) { }
public override string ConfigurationName => ConfigName;
}

View File

@@ -0,0 +1,18 @@
using MareSynchronos.MareConfiguration.Models;
namespace MareSynchronos.MareConfiguration.Configurations;
public class CharaDataConfig : IMareConfiguration
{
public bool OpenMareHubOnGposeStart { get; set; } = false;
public string LastSavedCharaDataLocation { get; set; } = string.Empty;
public Dictionary<string, CharaDataFavorite> FavoriteCodes { get; set; } = [];
public bool DownloadMcdDataOnConnection { get; set; } = true;
public int Version { get; set; } = 0;
public bool NearbyOwnServerOnly { get; set; } = false;
public bool NearbyIgnoreHousingLimitations { get; set; } = false;
public bool NearbyDrawWisps { get; set; } = true;
public int NearbyDistanceFilter { get; set; } = 100;
public bool NearbyShowOwnData { get; set; } = false;
public bool ShowHelpTexts { get; set; } = true;
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronos.MareConfiguration.Models;
[Serializable]
public class CharaDataFavorite
{
public DateTime LastDownloaded { get; set; } = DateTime.MaxValue;
public string CustomDescription { get; set; } = string.Empty;
}

View File

@@ -69,4 +69,8 @@
<None Include="..\.editorconfig" Link=".editorconfig" /> <None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="PlayerData\Export\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,267 +0,0 @@
using Dalamud.Game.ClientState.Objects.Types;
using K4os.Compression.LZ4.Legacy;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.FileCache;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Factories;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Logging;
using System.Text;
using CharacterData = MareSynchronos.API.Data.CharacterData;
namespace MareSynchronos.PlayerData.Export;
public class MareCharaFileManager : DisposableMediatorSubscriberBase
{
private readonly MareConfigService _configService;
private readonly DalamudUtilService _dalamudUtil;
private readonly MareCharaFileDataFactory _factory;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly Dictionary<string, GameObjectHandler> _gposeGameObjects;
private readonly List<Guid?> _gposeCustomizeObjects;
private readonly IpcManager _ipcManager;
private readonly ILogger<MareCharaFileManager> _logger;
private readonly FileCacheManager _manager;
private int _globalFileCounter = 0;
private bool _isInGpose = false;
public MareCharaFileManager(ILogger<MareCharaFileManager> logger, GameObjectHandlerFactory gameObjectHandlerFactory,
FileCacheManager manager, IpcManager ipcManager, MareConfigService configService, DalamudUtilService dalamudUtil,
MareMediator mediator) : base(logger, mediator)
{
_factory = new(manager);
_logger = logger;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_manager = manager;
_ipcManager = ipcManager;
_configService = configService;
_dalamudUtil = dalamudUtil;
_gposeGameObjects = [];
_gposeCustomizeObjects = [];
Mediator.Subscribe<GposeStartMessage>(this, _ => _isInGpose = true);
Mediator.Subscribe<GposeEndMessage>(this, async _ =>
{
_isInGpose = false;
CancellationTokenSource cts = new();
foreach (var item in _gposeGameObjects)
{
if ((await dalamudUtil.RunOnFrameworkThread(() => item.Value.CurrentAddress()).ConfigureAwait(false)) != nint.Zero)
{
await _ipcManager.Glamourer.RevertAsync(logger, item.Value, Guid.NewGuid(), cts.Token).ConfigureAwait(false);
}
else
{
_logger.LogDebug("Reverting by name: {name}", item.Key);
_ipcManager.Glamourer.RevertByName(logger, item.Key, Guid.NewGuid());
}
item.Value.Dispose();
}
foreach (var id in _gposeCustomizeObjects.Where(d => d != null))
{
await _ipcManager.CustomizePlus.RevertByIdAsync(id.Value);
}
_gposeGameObjects.Clear();
});
}
public bool CurrentlyWorking { get; private set; } = false;
public MareCharaFileHeader? LoadedCharaFile { get; private set; }
public async Task ApplyMareCharaFile(IGameObject? charaTarget, long expectedLength)
{
if (charaTarget == null) return;
Dictionary<string, string> extractedFiles = new(StringComparer.Ordinal);
CurrentlyWorking = true;
try
{
if (LoadedCharaFile == null || !File.Exists(LoadedCharaFile.FilePath)) return;
var unwrapped = File.OpenRead(LoadedCharaFile.FilePath);
await using (unwrapped.ConfigureAwait(false))
{
CancellationTokenSource disposeCts = new();
using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression);
using var reader = new BinaryReader(lz4Stream);
MareCharaFileHeader.AdvanceReaderToData(reader);
_logger.LogDebug("Applying to {chara}, expected length of contents: {exp}, stream length: {len}", charaTarget.Name.TextValue, expectedLength, reader.BaseStream.Length);
extractedFiles = ExtractFilesFromCharaFile(LoadedCharaFile, reader, expectedLength);
Dictionary<string, string> fileSwaps = new(StringComparer.Ordinal);
foreach (var fileSwap in LoadedCharaFile.CharaFileData.FileSwaps)
{
foreach (var path in fileSwap.GamePaths)
{
fileSwaps.Add(path, fileSwap.FileSwapPath);
}
}
var applicationId = Guid.NewGuid();
var coll = await _ipcManager.Penumbra.CreateTemporaryCollectionAsync(_logger, charaTarget.Name.TextValue).ConfigureAwait(false);
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(_logger, coll, charaTarget.ObjectIndex).ConfigureAwait(false);
await _ipcManager.Penumbra.SetTemporaryModsAsync(_logger, applicationId, coll, extractedFiles.Union(fileSwaps).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal)).ConfigureAwait(false);
await _ipcManager.Penumbra.SetManipulationDataAsync(_logger, applicationId, coll, LoadedCharaFile.CharaFileData.ManipulationData).ConfigureAwait(false);
GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
() => _dalamudUtil.GetGposeCharacterFromObjectTableByName(charaTarget.Name.ToString(), _isInGpose)?.Address ?? IntPtr.Zero, isWatched: false).ConfigureAwait(false);
if (!_gposeGameObjects.ContainsKey(charaTarget.Name.ToString()))
_gposeGameObjects[charaTarget.Name.ToString()] = tempHandler;
await _ipcManager.Glamourer.ApplyAllAsync(_logger, tempHandler, LoadedCharaFile.CharaFileData.GlamourerData, applicationId, disposeCts.Token).ConfigureAwait(false);
await _ipcManager.Penumbra.RedrawAsync(_logger, tempHandler, applicationId, disposeCts.Token).ConfigureAwait(false);
_dalamudUtil.WaitWhileGposeCharacterIsDrawing(charaTarget.Address, 30000);
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(_logger, applicationId, coll).ConfigureAwait(false);
if (!string.IsNullOrEmpty(LoadedCharaFile.CharaFileData.CustomizePlusData))
{
var id = await _ipcManager.CustomizePlus.SetBodyScaleAsync(tempHandler.Address, LoadedCharaFile.CharaFileData.CustomizePlusData).ConfigureAwait(false);
_gposeCustomizeObjects.Add(id);
}
else
{
var id = await _ipcManager.CustomizePlus.SetBodyScaleAsync(tempHandler.Address, Convert.ToBase64String(Encoding.UTF8.GetBytes("{}"))).ConfigureAwait(false);
_gposeCustomizeObjects.Add(id);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failure to read MCDF");
throw;
}
finally
{
CurrentlyWorking = false;
_logger.LogDebug("Clearing local files");
foreach (var file in Directory.EnumerateFiles(_configService.Current.CacheFolder, "*.tmp"))
{
File.Delete(file);
}
}
}
public void ClearMareCharaFile()
{
LoadedCharaFile = null;
}
public long LoadMareCharaFile(string filePath)
{
CurrentlyWorking = true;
try
{
using var unwrapped = File.OpenRead(filePath);
using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression);
using var reader = new BinaryReader(lz4Stream);
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());
}
return expectedLength;
}
finally { CurrentlyWorking = false; }
}
public void SaveMareCharaFile(CharacterData? dto, string description, string filePath)
{
CurrentlyWorking = true;
var tempFilePath = filePath + ".tmp";
try
{
if (dto == null) return;
var mareCharaFileData = _factory.Create(description, dto);
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 = _manager.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);
}
using var fsRead = File.OpenRead(file.ResolvedFilepath);
using var br = new BinaryReader(fsRead);
byte[] buffer = new byte[item.Length];
br.Read(buffer, 0, item.Length);
writer.Write(buffer);
}
writer.Flush();
lz4.Flush();
fs.Flush();
fs.Close();
File.Move(tempFilePath, filePath, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failure Saving Mare Chara File, deleting output");
File.Delete(tempFilePath);
}
finally { CurrentlyWorking = false; }
}
private Dictionary<string, string> ExtractFilesFromCharaFile(MareCharaFileHeader charaFileHeader, BinaryReader reader, long expectedLength)
{
long totalRead = 0;
Dictionary<string, string> gamePathToFilePath = new(StringComparer.Ordinal);
foreach (var fileData in charaFileHeader.CharaFileData.Files)
{
var fileName = Path.Combine(_configService.Current.CacheFolder, "mare_" + _globalFileCounter++ + ".tmp");
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;
}
}

View File

@@ -23,6 +23,7 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
private CancellationTokenSource _petNicknamesCts = new(); private CancellationTokenSource _petNicknamesCts = new();
private CancellationTokenSource _moodlesCts = new(); private CancellationTokenSource _moodlesCts = new();
private bool _isZoning = false; private bool _isZoning = false;
private bool _haltCharaDataCreation;
private readonly Dictionary<ObjectKind, CancellationTokenSource> _glamourerCts = new(); private readonly Dictionary<ObjectKind, CancellationTokenSource> _glamourerCts = new();
public CacheCreationService(ILogger<CacheCreationService> logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory, public CacheCreationService(ILogger<CacheCreationService> logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory,
@@ -41,6 +42,11 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (msg) => _isZoning = true); Mediator.Subscribe<ZoneSwitchStartMessage>(this, (msg) => _isZoning = true);
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (msg) => _isZoning = false); Mediator.Subscribe<ZoneSwitchEndMessage>(this, (msg) => _isZoning = false);
Mediator.Subscribe<HaltCharaDataCreation>(this, (msg) =>
{
_haltCharaDataCreation = !msg.Resume;
});
_playerRelatedObjects[ObjectKind.Player] = gameObjectHandlerFactory.Create(ObjectKind.Player, dalamudUtil.GetPlayerPointer, isWatched: true) _playerRelatedObjects[ObjectKind.Player] = gameObjectHandlerFactory.Create(ObjectKind.Player, dalamudUtil.GetPlayerPointer, isWatched: true)
.GetAwaiter().GetResult(); .GetAwaiter().GetResult();
_playerRelatedObjects[ObjectKind.MinionOrMount] = gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), isWatched: true) _playerRelatedObjects[ObjectKind.MinionOrMount] = gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), isWatched: true)
@@ -218,7 +224,7 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
private void ProcessCacheCreation() private void ProcessCacheCreation()
{ {
if (_isZoning) return; if (_isZoning || _haltCharaDataCreation) return;
if (_cachesToCreate.Any() && (_cacheCreationTask?.IsCompleted ?? true)) if (_cachesToCreate.Any() && (_cacheCreationTask?.IsCompleted ?? true))
{ {

View File

@@ -8,7 +8,6 @@ using MareSynchronos.Interop;
using MareSynchronos.Interop.Ipc; using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Configurations; using MareSynchronos.MareConfiguration.Configurations;
using MareSynchronos.PlayerData.Export;
using MareSynchronos.PlayerData.Factories; using MareSynchronos.PlayerData.Factories;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.PlayerData.Services; using MareSynchronos.PlayerData.Services;
@@ -93,7 +92,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<FileCacheManager>(); collection.AddSingleton<FileCacheManager>();
collection.AddSingleton<ServerConfigurationManager>(); collection.AddSingleton<ServerConfigurationManager>();
collection.AddSingleton<ApiController>(); collection.AddSingleton<ApiController>();
collection.AddSingleton<MareCharaFileManager>();
collection.AddSingleton<PerformanceCollectorService>(); collection.AddSingleton<PerformanceCollectorService>();
collection.AddSingleton<HubFactory>(); collection.AddSingleton<HubFactory>();
collection.AddSingleton<FileUploadManager>(); collection.AddSingleton<FileUploadManager>();
@@ -112,10 +110,17 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<FileCompactor>(); collection.AddSingleton<FileCompactor>();
collection.AddSingleton<TagHandler>(); collection.AddSingleton<TagHandler>();
collection.AddSingleton<UidDisplayHandler>(); collection.AddSingleton<UidDisplayHandler>();
collection.AddSingleton<BlockedCharacterHandler>();
collection.AddSingleton<IpcProvider>();
collection.AddSingleton<PluginWatcherService>(); collection.AddSingleton<PluginWatcherService>();
collection.AddSingleton<PlayerPerformanceService>(); collection.AddSingleton<PlayerPerformanceService>();
collection.AddSingleton<CharaDataManager>();
collection.AddSingleton<CharaDataFileHandler>();
collection.AddSingleton<CharaDataCharacterHandler>();
collection.AddSingleton<CharaDataNearbyManager>();
collection.AddSingleton<VfxSpawnManager>();
collection.AddSingleton<BlockedCharacterHandler>();
collection.AddSingleton<IpcProvider>();
collection.AddSingleton<VisibilityService>(); collection.AddSingleton<VisibilityService>();
collection.AddSingleton<EventAggregator>(); collection.AddSingleton<EventAggregator>();
collection.AddSingleton<DalamudUtilService>(); collection.AddSingleton<DalamudUtilService>();
@@ -127,8 +132,9 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<IpcCallerCustomize>(); collection.AddSingleton<IpcCallerCustomize>();
collection.AddSingleton<IpcCallerHeels>(); collection.AddSingleton<IpcCallerHeels>();
collection.AddSingleton<IpcCallerHonorific>(); collection.AddSingleton<IpcCallerHonorific>();
collection.AddSingleton<IpcCallerPetNames>();
collection.AddSingleton<IpcCallerMoodles>(); collection.AddSingleton<IpcCallerMoodles>();
collection.AddSingleton<IpcCallerPetNames>();
collection.AddSingleton<IpcCallerBrio>();
collection.AddSingleton<IpcManager>(); collection.AddSingleton<IpcManager>();
collection.AddSingleton<NotificationService>(); collection.AddSingleton<NotificationService>();
@@ -141,6 +147,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<MareConfigService>()); collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<MareConfigService>());
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>()); collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>()); collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
@@ -150,6 +157,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<XivDataStorageService>()); collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<XivDataStorageService>());
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<PlayerPerformanceConfigService>()); collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<PlayerPerformanceConfigService>());
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerBlockConfigService>()); collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerBlockConfigService>());
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
collection.AddSingleton<ConfigurationMigrator>(); collection.AddSingleton<ConfigurationMigrator>();
collection.AddSingleton<ConfigurationSaveService>(); collection.AddSingleton<ConfigurationSaveService>();
@@ -160,12 +168,12 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped<UiFactory>(); collection.AddScoped<UiFactory>();
collection.AddScoped<WindowMediatorSubscriberBase, SettingsUi>(); collection.AddScoped<WindowMediatorSubscriberBase, SettingsUi>();
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>(); collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
collection.AddScoped<WindowMediatorSubscriberBase, GposeUi>();
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>(); collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>(); collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>(); collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>(); collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>(); collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>(); collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>();
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>(); collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
collection.AddScoped<IPopupHandler, ReportPopupHandler>(); collection.AddScoped<IPopupHandler, ReportPopupHandler>();

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
using MareSynchronos.FileCache; using MareSynchronos.FileCache;
using MareSynchronos.Services.CharaData.Models;
namespace MareSynchronos.PlayerData.Export; namespace MareSynchronos.Services.CharaData;
internal sealed class MareCharaFileDataFactory internal sealed class MareCharaFileDataFactory
{ {

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ using MareSynchronos.FileCache;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
namespace MareSynchronos.PlayerData.Export; namespace MareSynchronos.Services.CharaData.Models;
public record MareCharaFileData public record MareCharaFileData
{ {

View File

@@ -1,4 +1,4 @@
namespace MareSynchronos.PlayerData.Export; namespace MareSynchronos.Services.CharaData.Models;
public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData) public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData)
{ {

View File

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

View File

@@ -107,7 +107,7 @@ public sealed class CommandManagerService : IDisposable
} }
else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase)) 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)) else if (string.Equals(splitArgs[0], "rescan", StringComparison.OrdinalIgnoreCase))
{ {

View File

@@ -1,10 +1,16 @@
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; 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.Interop;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
@@ -13,6 +19,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject; 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)) .Where(x => x.RowId != 0 && !(x.RowId >= 500 && (x.Dark & 0xFFFFFF00) == 0))
.ToDictionary(x => (int)x.RowId); .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) => mediator.Subscribe<TargetPairMessage>(this, (msg) =>
{ {
if (clientState.IsPvP) return; if (clientState.IsPvP) return;
@@ -103,7 +147,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
} }
public bool IsWine { get; init; } 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 unsafe Dalamud.Game.ClientState.Objects.Types.IGameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex];
public bool IsAnythingDrawing { get; private set; } = false; public bool IsAnythingDrawing { get; private set; } = false;
public bool IsInCutscene { 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<ushort, string>> WorldData { get; private set; }
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { 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; } public MareMediator Mediator { get; }
@@ -157,13 +207,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return await RunOnFrameworkThread(() => GetCompanion(playerPointer)).ConfigureAwait(false); 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(); 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)); .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() public bool GetIsPlayerPresent()
{ {
EnsureIsOnFramework(); EnsureIsOnFramework();
@@ -203,6 +263,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return await RunOnFrameworkThread(() => GetPet(playerPointer)).ConfigureAwait(false); 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) public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName)
{ {
if (_playerCharas.TryGetValue(characterName, out var pchar)) return pchar.Address; if (_playerCharas.TryGetValue(characterName, out var pchar)) return pchar.Address;
@@ -248,6 +319,60 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return _clientState.LocalPlayer!.CurrentWorld.RowId; 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() public async Task<uint> GetWorldIdAsync()
{ {
return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false); return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false);
@@ -274,7 +399,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false); 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); var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
await _performanceCollector.LogPerformance(this, $"RunOnFramework:Act/{fileName}>{callerMember}:{callerLineNumber}", async () => await _performanceCollector.LogPerformance(this, $"RunOnFramework:Act/{fileName}>{callerMember}:{callerLineNumber}", async () =>
@@ -534,13 +659,13 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_lastGlobalBlockReason = string.Empty; _lastGlobalBlockReason = string.Empty;
} }
if (GposeTarget != null && !IsInGpose) if (_clientState.IsGPosing && !IsInGpose)
{ {
_logger.LogDebug("Gpose start"); _logger.LogDebug("Gpose start");
IsInGpose = true; IsInGpose = true;
Mediator.Publish(new GposeStartMessage()); Mediator.Publish(new GposeStartMessage());
} }
else if (GposeTarget == null && IsInGpose) else if (!_clientState.IsGPosing && IsInGpose)
{ {
_logger.LogDebug("Gpose end"); _logger.LogDebug("Gpose end");
IsInGpose = false; IsInGpose = false;

View File

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

View File

@@ -0,0 +1,194 @@
using Dalamud.Interface.Utility.Raii;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.CharaData.Models;
using System.Text;
namespace MareSynchronos.UI;
internal sealed partial class CharaDataHubUi
{
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
{
AccessTypeDto.AllPairs => "All Pairs",
AccessTypeDto.ClosePairs => "Close Pairs",
AccessTypeDto.Individuals => "Specified",
AccessTypeDto.Public => "Everyone"
};
private static string GetShareTypeString(ShareTypeDto dto) => dto switch
{
ShareTypeDto.Private => "Private",
ShareTypeDto.Shared => "Shared"
};
private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry)
{
if (!poseEntry.HasWorldData) return "This Pose has no world data attached.";
return poseEntry.WorldDataDescriptor;
}
private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(actionDescription);
bool isDisabled = false;
void AddErrorStart(StringBuilder sb)
{
sb.Append(UiSharedService.TooltipSeparator);
sb.AppendLine("Cannot execute:");
}
if (dto == null)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- No metainfo present");
isDisabled = true;
}
if (!dto?.CanBeDownloaded ?? false)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Character is not downloadable");
isDisabled = true;
}
if (!_uiSharedService.IsInGpose)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires to be in GPose");
isDisabled = true;
}
if (!hasValidGposeTarget && !isSpawning)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires a valid GPose target");
isDisabled = true;
}
if (isSpawning && !_charaDataManager.BrioAvailable)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires Brio to be installed.");
isDisabled = true;
}
using (ImRaii.Group())
{
using var dis = ImRaii.Disabled(isDisabled);
gposeActionDraw.Invoke(dto);
}
if (sb.Length > 0)
{
UiSharedService.AttachToolTip(sb.ToString());
}
}
private void GposePoseAction(Action poseActionDraw, string poseDescription, bool hasValidGposeTarget)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(poseDescription);
bool isDisabled = false;
void AddErrorStart(StringBuilder sb)
{
sb.Append(UiSharedService.TooltipSeparator);
sb.AppendLine("Cannot execute:");
}
if (!_uiSharedService.IsInGpose)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires to be in GPose");
isDisabled = true;
}
if (!hasValidGposeTarget)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires a valid GPose target");
isDisabled = true;
}
if (!_charaDataManager.BrioAvailable)
{
if (!isDisabled) AddErrorStart(sb);
sb.AppendLine("- Requires Brio to be installed.");
isDisabled = true;
}
using (ImRaii.Group())
{
using var dis = ImRaii.Disabled(isDisabled);
poseActionDraw.Invoke();
}
if (sb.Length > 0)
{
UiSharedService.AttachToolTip(sb.ToString());
}
}
private void SetWindowSizeConstraints(bool? inGposeTab = null)
{
SizeConstraints = new()
{
MinimumSize = new((inGposeTab ?? false) ? 400 : 1000, 500),
MaximumSize = new((inGposeTab ?? false) ? 400 : 1000, 2000)
};
}
private void UpdateFilteredFavorites()
{
_ = Task.Run(async () =>
{
if (_charaDataManager.DownloadMetaInfoTask != null)
{
await _charaDataManager.DownloadMetaInfoTask.ConfigureAwait(false);
}
Dictionary<string, (CharaDataFavorite, CharaDataMetaInfoExtendedDto?, bool)> newFiltered = [];
foreach (var favorite in _configService.Current.FavoriteCodes)
{
var uid = favorite.Key.Split(":")[0];
var note = _serverConfigurationManager.GetNoteForUid(uid) ?? string.Empty;
bool hasMetaInfo = _charaDataManager.TryGetMetaInfo(favorite.Key, out var metaInfo);
bool addFavorite =
(string.IsNullOrEmpty(_filterCodeNote)
|| (note.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)
|| uid.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase)))
&& (string.IsNullOrEmpty(_filterDescription)
|| (favorite.Value.CustomDescription.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase)
|| (metaInfo != null && metaInfo!.Description.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase))))
&& (!_filterPoseOnly
|| (metaInfo != null && metaInfo!.HasPoses))
&& (!_filterWorldOnly
|| (metaInfo != null && metaInfo!.HasWorldData));
if (addFavorite)
{
newFiltered[favorite.Key] = (favorite.Value, metaInfo, hasMetaInfo);
}
}
_filteredFavorites = newFiltered;
});
}
private void UpdateFilteredItems()
{
if (_charaDataManager.GetSharedWithYouTask == null)
{
_filteredDict = _charaDataManager.SharedWithYouData
.SelectMany(k => k.Value)
.Where(k =>
(!_sharedWithYouDownloadableFilter || k.CanBeDownloaded)
&& (string.IsNullOrEmpty(_sharedWithYouDescriptionFilter) || k.Description.Contains(_sharedWithYouDescriptionFilter, StringComparison.OrdinalIgnoreCase)))
.GroupBy(k => k.Uploader)
.ToDictionary(k =>
{
var note = _serverConfigurationManager.GetNoteForUid(k.Key.UID);
if (note == null) return k.Key.AliasOrUID;
return $"{note} ({k.Key.AliasOrUID})";
}, k => k.ToList(), StringComparer.OrdinalIgnoreCase)
.Where(k => (string.IsNullOrEmpty(_sharedWithYouOwnerFilter) || k.Key.Contains(_sharedWithYouOwnerFilter, StringComparison.OrdinalIgnoreCase)))
.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToDictionary();
}
}
}

View File

@@ -0,0 +1,738 @@
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Utility;
using Dalamud.Interface;
using ImGuiNET;
using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.Services.CharaData.Models;
using System.Numerics;
namespace MareSynchronos.UI;
internal sealed partial class CharaDataHubUi
{
private void DrawEditCharaData(CharaDataFullExtendedDto? dataDto)
{
using var imguiid = ImRaii.PushId(dataDto?.Id ?? "NoData");
if (dataDto == null)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", ImGuiColors.DalamudYellow);
return;
}
var updateDto = _charaDataManager.GetUpdateDto(dataDto.Id);
if (updateDto == null)
{
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", ImGuiColors.DalamudYellow);
return;
}
bool canUpdate = updateDto.HasChanges;
if (canUpdate || _charaDataManager.CharaUpdateTask != null)
{
ImGuiHelpers.ScaledDummy(5);
}
var indent = ImRaii.PushIndent(10f);
if (canUpdate || (!_charaDataManager.UploadTask?.IsCompleted ?? false))
{
UiSharedService.DrawGrouped(() =>
{
if (canUpdate)
{
ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", ImGuiColors.DalamudRed);
ImGui.SameLine();
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Save to Server"))
{
_charaDataManager.UploadCharaData(dataDto.Id);
}
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Undo, "Undo all changes"))
{
updateDto.UndoChanges();
}
}
if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", ImGuiColors.DalamudYellow);
}
}
if (!_charaDataManager.UploadTask?.IsCompleted ?? false)
{
DisableDisabled(() =>
{
if (_charaDataManager.UploadProgress != null)
{
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, ImGuiColors.DalamudYellow);
}
if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload"))
{
_charaDataManager.CancelUpload();
}
else if (_charaDataManager.UploadTask?.IsCompleted ?? false)
{
var color = UiSharedService.GetBoolColor(_charaDataManager.UploadTask.Result.Success);
UiSharedService.ColorTextWrapped(_charaDataManager.UploadTask.Result.Output, color);
}
});
}
});
}
indent.Dispose();
if (canUpdate || _charaDataManager.CharaUpdateTask != null)
{
ImGuiHelpers.ScaledDummy(5);
}
using var child = ImRaii.Child("editChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize);
DrawEditCharaDataGeneral(dataDto, updateDto);
ImGuiHelpers.ScaledDummy(5);
DrawEditCharaDataAccessAndSharing(updateDto);
ImGuiHelpers.ScaledDummy(5);
DrawEditCharaDataAppearance(dataDto, updateDto);
ImGuiHelpers.ScaledDummy(5);
DrawEditCharaDataPoses(updateDto);
}
private void DrawEditCharaDataAccessAndSharing(CharaDataExtendedUpdateDto updateDto)
{
_uiSharedService.BigText("Access and Sharing");
ImGui.SetNextItemWidth(200);
var dtoAccessType = updateDto.AccessType;
if (ImGui.BeginCombo("Access Restrictions", GetAccessTypeString(dtoAccessType)))
{
foreach (var accessType in Enum.GetValues(typeof(AccessTypeDto)).Cast<AccessTypeDto>())
{
if (ImGui.Selectable(GetAccessTypeString(accessType), accessType == dtoAccessType))
{
updateDto.AccessType = accessType;
}
}
ImGui.EndCombo();
}
_uiSharedService.DrawHelpText("You can control who has access to your character data based on the access restrictions." + UiSharedService.TooltipSeparator
+ "Specified: Only people you directly specify in 'Specific Individuals' can access this character data" + Environment.NewLine
+ "Close Pairs: Only people you have directly paired can access this character data" + Environment.NewLine
+ "All Pairs: All people you have paired can access this character data" + Environment.NewLine
+ "Everyone: Everyone can access this character data" + UiSharedService.TooltipSeparator
+ "Note: To access your character data the person in question requires to have the code. Exceptions for 'Shared' data, see 'Sharing' below." + Environment.NewLine
+ "Note: For 'Close' and 'All Pairs' the pause state plays a role. Paused people will not be able to access your character data." + Environment.NewLine
+ "Note: Directly specified individuals in the 'Specific Individuals' list will be able to access your character data regardless of pause or pair state.");
DrawSpecificIndividuals(updateDto);
ImGui.SetNextItemWidth(200);
var dtoShareType = updateDto.ShareType;
using (ImRaii.Disabled(dtoAccessType == AccessTypeDto.Public))
{
if (ImGui.BeginCombo("Sharing", GetShareTypeString(dtoShareType)))
{
foreach (var shareType in Enum.GetValues(typeof(ShareTypeDto)).Cast<ShareTypeDto>())
{
if (ImGui.Selectable(GetShareTypeString(shareType), shareType == dtoShareType))
{
updateDto.ShareType = shareType;
}
}
ImGui.EndCombo();
}
}
_uiSharedService.DrawHelpText("This regulates how you want to distribute this character data." + UiSharedService.TooltipSeparator
+ "Private: People require to have the code to download this character data" + Environment.NewLine
+ "Shared: People that are allowed through 'Access Restrictions' will have this character data entry displayed in 'Shared with You'" + UiSharedService.TooltipSeparator
+ "Note: Shared is incompatible with Access Restriction 'Everyone'");
ImGuiHelpers.ScaledDummy(10f);
}
private void DrawEditCharaDataAppearance(CharaDataFullExtendedDto dataDto, CharaDataExtendedUpdateDto updateDto)
{
_uiSharedService.BigText("Appearance");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Set Appearance to Current Appearance"))
{
_charaDataManager.SetAppearanceData(dataDto.Id);
}
_uiSharedService.DrawHelpText("This will overwrite the appearance data currently stored in this Character Data entry with your current appearance.");
ImGui.SameLine();
using (ImRaii.Disabled(dataDto.HasMissingFiles || !updateDto.IsAppearanceEqual || _charaDataManager.DataApplicationTask != null))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.CheckCircle, "Preview Saved Apperance on Self"))
{
_charaDataManager.ApplyDataToSelf(dataDto);
}
}
_uiSharedService.DrawHelpText("This will download and apply the saved character data to yourself. Once loaded it will automatically revert itself within 15 seconds." + UiSharedService.TooltipSeparator
+ "Note: Weapons will not be displayed correctly unless using the same job as the saved data.");
ImGui.TextUnformatted("Contains Glamourer Data");
ImGui.SameLine();
bool hasGlamourerdata = !string.IsNullOrEmpty(updateDto.GlamourerData);
ImGui.SameLine(200);
_uiSharedService.BooleanToColoredIcon(hasGlamourerdata, false);
ImGui.TextUnformatted("Contains Files");
var hasFiles = (updateDto.FileGamePaths ?? []).Any() || (dataDto.OriginalFiles.Any());
ImGui.SameLine(200);
_uiSharedService.BooleanToColoredIcon(hasFiles, false);
if (hasFiles && updateDto.IsAppearanceEqual)
{
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20, 1);
ImGui.SameLine();
var pos = ImGui.GetCursorPosX();
ImGui.NewLine();
ImGui.SameLine(pos);
ImGui.TextUnformatted($"{dataDto.FileGamePaths.DistinctBy(k => k.HashOrFileSwap).Count()} unique file hashes (original upload: {dataDto.OriginalFiles.DistinctBy(k => k.HashOrFileSwap).Count()} file hashes)");
ImGui.NewLine();
ImGui.SameLine(pos);
ImGui.TextUnformatted($"{dataDto.FileGamePaths.Count} associated game paths");
ImGui.NewLine();
ImGui.SameLine(pos);
ImGui.TextUnformatted($"{dataDto.FileSwaps!.Count} file swaps");
ImGui.NewLine();
ImGui.SameLine(pos);
if (!dataDto.HasMissingFiles)
{
UiSharedService.ColorTextWrapped("All files to download this character data are present on the server", ImGuiColors.HealerGreen);
}
else
{
UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", ImGuiColors.DalamudRed);
ImGui.NewLine();
ImGui.SameLine(pos);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data"))
{
_charaDataManager.UploadMissingFiles(dataDto.Id);
}
}
}
else if (hasFiles && !updateDto.IsAppearanceEqual)
{
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20, 1);
ImGui.SameLine();
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", ImGuiColors.DalamudYellow);
}
ImGui.TextUnformatted("Contains Manipulation Data");
bool hasManipData = !string.IsNullOrEmpty(updateDto.ManipulationData);
ImGui.SameLine(200);
_uiSharedService.BooleanToColoredIcon(hasManipData, false);
ImGui.TextUnformatted("Contains Customize+ Data");
ImGui.SameLine();
bool hasCustomizeData = !string.IsNullOrEmpty(updateDto.CustomizeData);
ImGui.SameLine(200);
_uiSharedService.BooleanToColoredIcon(hasCustomizeData, false);
}
private void DrawEditCharaDataGeneral(CharaDataFullExtendedDto dataDto, CharaDataExtendedUpdateDto updateDto)
{
_uiSharedService.BigText("General");
string code = dataDto.FullId;
using (ImRaii.Disabled())
{
ImGui.SetNextItemWidth(200);
ImGui.InputText("##CharaDataCode", ref code, 255, ImGuiInputTextFlags.ReadOnly);
}
ImGui.SameLine();
ImGui.TextUnformatted("Chara Data Code");
ImGui.SameLine();
if (_uiSharedService.IconButton(FontAwesomeIcon.Copy))
{
ImGui.SetClipboardText(code);
}
UiSharedService.AttachToolTip("Copy Code to Clipboard");
string creationTime = dataDto.CreatedDate.ToLocalTime().ToString();
string updateTime = dataDto.UpdatedDate.ToLocalTime().ToString();
string downloadCount = dataDto.DownloadCount.ToString();
using (ImRaii.Disabled())
{
ImGui.SetNextItemWidth(200);
ImGui.InputText("##CreationDate", ref creationTime, 255, ImGuiInputTextFlags.ReadOnly);
}
ImGui.SameLine();
ImGui.TextUnformatted("Creation Date");
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20);
ImGui.SameLine();
using (ImRaii.Disabled())
{
ImGui.SetNextItemWidth(200);
ImGui.InputText("##LastUpdate", ref updateTime, 255, ImGuiInputTextFlags.ReadOnly);
}
ImGui.SameLine();
ImGui.TextUnformatted("Last Update Date");
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(23);
ImGui.SameLine();
using (ImRaii.Disabled())
{
ImGui.SetNextItemWidth(50);
ImGui.InputText("##DlCount", ref downloadCount, 255, ImGuiInputTextFlags.ReadOnly);
}
ImGui.SameLine();
ImGui.TextUnformatted("Download Count");
string description = updateDto.Description;
ImGui.SetNextItemWidth(735);
if (ImGui.InputText("##Description", ref description, 200))
{
updateDto.Description = description;
}
ImGui.SameLine();
ImGui.TextUnformatted("Description");
_uiSharedService.DrawHelpText("Description for this Character Data." + UiSharedService.TooltipSeparator
+ "Note: the description will be visible to anyone who can access this character data. See 'Access Restrictions' and 'Sharing' below.");
var expiryDate = updateDto.ExpiryDate;
bool isExpiring = expiryDate != DateTime.MaxValue;
if (ImGui.Checkbox("Expires", ref isExpiring))
{
updateDto.SetExpiry(isExpiring);
}
_uiSharedService.DrawHelpText("If expiration is enabled, the uploaded character data will be automatically deleted from the server at the specified date.");
using (ImRaii.Disabled(!isExpiring))
{
ImGui.SameLine();
ImGui.SetNextItemWidth(100);
if (ImGui.BeginCombo("Year", expiryDate.Year.ToString()))
{
for (int year = DateTime.UtcNow.Year; year < DateTime.UtcNow.Year + 4; year++)
{
if (ImGui.Selectable(year.ToString(), year == expiryDate.Year))
{
updateDto.SetExpiry(year, expiryDate.Month, expiryDate.Day);
}
}
ImGui.EndCombo();
}
ImGui.SameLine();
int daysInMonth = DateTime.DaysInMonth(expiryDate.Year, expiryDate.Month);
ImGui.SetNextItemWidth(100);
if (ImGui.BeginCombo("Month", expiryDate.Month.ToString()))
{
for (int month = 1; month <= 12; month++)
{
if (ImGui.Selectable(month.ToString(), month == expiryDate.Month))
{
updateDto.SetExpiry(expiryDate.Year, month, expiryDate.Day);
}
}
ImGui.EndCombo();
}
ImGui.SameLine();
ImGui.SetNextItemWidth(100);
if (ImGui.BeginCombo("Day", expiryDate.Day.ToString()))
{
for (int day = 1; day <= daysInMonth; day++)
{
if (ImGui.Selectable(day.ToString(), day == expiryDate.Day))
{
updateDto.SetExpiry(expiryDate.Year, expiryDate.Month, day);
}
}
ImGui.EndCombo();
}
}
ImGuiHelpers.ScaledDummy(5);
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Character Data"))
{
_ = _charaDataManager.DeleteCharaData(dataDto);
_selectedDtoId = string.Empty;
}
}
if (!UiSharedService.CtrlPressed())
{
UiSharedService.AttachToolTip("Hold CTRL and click to delete the current data. This operation is irreversible.");
}
}
private void DrawEditCharaDataPoses(CharaDataExtendedUpdateDto updateDto)
{
_uiSharedService.BigText("Poses");
var poseCount = updateDto.PoseList.Count();
using (ImRaii.Disabled(poseCount >= maxPoses))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Pose"))
{
updateDto.AddPose();
}
}
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, poseCount == maxPoses))
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
ImGuiHelpers.ScaledDummy(5);
using var indent = ImRaii.PushIndent(10f);
int poseNumber = 1;
if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", ImGuiColors.DalamudYellow);
ImGuiHelpers.ScaledDummy(5);
}
else if (!_charaDataManager.BrioAvailable)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", ImGuiColors.DalamudRed);
ImGuiHelpers.ScaledDummy(5);
}
foreach (var pose in updateDto.PoseList)
{
ImGui.AlignTextToFramePadding();
using var id = ImRaii.PushId("pose" + poseNumber);
ImGui.TextUnformatted(poseNumber.ToString());
if (pose.Id == null)
{
ImGui.SameLine(50);
_uiSharedService.IconText(FontAwesomeIcon.Plus, ImGuiColors.DalamudYellow);
UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data.");
}
bool poseHasChanges = updateDto.PoseHasChanges(pose);
if (poseHasChanges)
{
ImGui.SameLine(50);
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet.");
}
ImGui.SameLine(75);
if (pose.Description == null && pose.WorldData == null && pose.PoseData == null)
{
UiSharedService.ColorText("Pose scheduled for deletion", ImGuiColors.DalamudYellow);
}
else
{
var desc = pose.Description;
if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100))
{
pose.Description = desc;
updateDto.UpdatePoseList();
}
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete"))
{
updateDto.RemovePose(pose);
}
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(10, 1);
ImGui.SameLine();
bool hasPoseData = !string.IsNullOrEmpty(pose.PoseData);
_uiSharedService.IconText(FontAwesomeIcon.Running, UiSharedService.GetBoolColor(hasPoseData));
UiSharedService.AttachToolTip(hasPoseData
? "This Pose entry has pose data attached"
: "This Pose entry has no pose data attached");
ImGui.SameLine();
using (ImRaii.Disabled(!_uiSharedService.IsInGpose || !(_charaDataManager.AttachingPoseTask?.IsCompleted ?? true) || !_charaDataManager.BrioAvailable))
{
using var poseid = ImRaii.PushId("poseSet" + poseNumber);
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
_charaDataManager.AttachPoseData(pose, updateDto);
}
UiSharedService.AttachToolTip("Apply current pose data to pose");
}
ImGui.SameLine();
using (ImRaii.Disabled(!hasPoseData))
{
using var poseid = ImRaii.PushId("poseDelete" + poseNumber);
if (_uiSharedService.IconButton(FontAwesomeIcon.Trash))
{
pose.PoseData = string.Empty;
updateDto.UpdatePoseList();
}
UiSharedService.AttachToolTip("Delete current pose data from pose");
}
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(10, 1);
ImGui.SameLine();
var worldData = pose.WorldData;
bool hasWorldData = (worldData ?? default) != default;
_uiSharedService.IconText(FontAwesomeIcon.Globe, UiSharedService.GetBoolColor(hasWorldData));
var tooltipText = !hasWorldData ? "This Pose has no world data attached." : "This Pose has world data attached.";
if (hasWorldData)
{
tooltipText += UiSharedService.TooltipSeparator + "Click to show location on map";
}
UiSharedService.AttachToolTip(tooltipText);
if (hasWorldData && ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
_dalamudUtilService.SetMarkerAndOpenMap(position: new Vector3(worldData.Value.PositionX, worldData.Value.PositionY, worldData.Value.PositionZ),
_dalamudUtilService.MapData.Value[worldData.Value.LocationInfo.MapId].Map);
}
ImGui.SameLine();
using (ImRaii.Disabled(!_uiSharedService.IsInGpose || !(_charaDataManager.AttachingPoseTask?.IsCompleted ?? true) || !_charaDataManager.BrioAvailable))
{
using var worldId = ImRaii.PushId("worldSet" + poseNumber);
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
_charaDataManager.AttachWorldData(pose, updateDto);
}
UiSharedService.AttachToolTip("Apply current world position data to pose");
}
ImGui.SameLine();
using (ImRaii.Disabled(!hasWorldData))
{
using var worldId = ImRaii.PushId("worldDelete" + poseNumber);
if (_uiSharedService.IconButton(FontAwesomeIcon.Trash))
{
pose.WorldData = default(WorldData);
updateDto.UpdatePoseList();
}
UiSharedService.AttachToolTip("Delete current world position data from pose");
}
}
if (poseHasChanges)
{
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Undo, "Undo"))
{
updateDto.RevertDeletion(pose);
}
}
poseNumber++;
}
}
private void DrawMcdOnline()
{
_uiSharedService.BigText("Mare Character Data Online");
DrawHelpFoldout("In this tab you can create, view and edit your own Mare Character Data that is stored on the server." + Environment.NewLine + Environment.NewLine
+ "Mare Character Data Online functions similar to the previous MCDF standard for exporting your character, except that you do not have to send a file to the other person but solely a code." + Environment.NewLine + Environment.NewLine
+ "There would be a bit too much to explain here on what you can do here in its entirety, however, all elements in this tab have help texts attached what they are used for. Please review them carefully." + Environment.NewLine + Environment.NewLine
+ "Be mindful that when you share your Character Data with other people there is a chance that, with the help of unsanctioned 3rd party plugins, your appearance could be stolen irreversibly, just like when using MCDF.");
ImGuiHelpers.ScaledDummy(5);
using (ImRaii.Disabled(_charaDataManager.GetAllDataTask != null
|| (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Character Data from Server"))
{
_ = _charaDataManager.GetAllData(_disposalCts.Token);
}
}
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
{
UiSharedService.AttachToolTip("You can only refresh all character data from server every minute. Please wait.");
}
using (var table = ImRaii.Table("Own Character Data", 12, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY,
new Vector2(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X, 100)))
{
if (table)
{
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 18);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 18);
ImGui.TableSetupColumn("Code");
ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Created");
ImGui.TableSetupColumn("Updated");
ImGui.TableSetupColumn("Download Count", ImGuiTableColumnFlags.WidthFixed, 18);
ImGui.TableSetupColumn("Downloadable", ImGuiTableColumnFlags.WidthFixed, 18);
ImGui.TableSetupColumn("Files", ImGuiTableColumnFlags.WidthFixed, 32);
ImGui.TableSetupColumn("Glamourer", ImGuiTableColumnFlags.WidthFixed, 18);
ImGui.TableSetupColumn("Customize+", ImGuiTableColumnFlags.WidthFixed, 18);
ImGui.TableSetupColumn("Expires", ImGuiTableColumnFlags.WidthFixed, 18);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
foreach (var entry in _charaDataManager.OwnCharaData.Values)
{
var uDto = _charaDataManager.GetUpdateDto(entry.Id);
ImGui.TableNextColumn();
if (string.Equals(entry.Id, _selectedDtoId, StringComparison.Ordinal))
_uiSharedService.IconText(FontAwesomeIcon.CaretRight);
ImGui.TableNextColumn();
DrawAddOrRemoveFavorite(entry);
ImGui.TableNextColumn();
var idText = entry.FullId;
if (uDto?.HasChanges ?? false)
{
UiSharedService.ColorText(idText, ImGuiColors.DalamudYellow);
UiSharedService.AttachToolTip("This entry has unsaved changes");
}
else
{
ImGui.TextUnformatted(idText);
}
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.Description);
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
UiSharedService.AttachToolTip(entry.Description);
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.CreatedDate.ToLocalTime().ToString());
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.UpdatedDate.ToLocalTime().ToString());
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.DownloadCount.ToString());
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
ImGui.TableNextColumn();
bool isDownloadable = !entry.HasMissingFiles
&& !string.IsNullOrEmpty(entry.GlamourerData);
_uiSharedService.BooleanToColoredIcon(isDownloadable, false);
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
UiSharedService.AttachToolTip(isDownloadable ? "Can be downloaded by others" : "Cannot be downloaded: Has missing files or data, please review this entry manually");
ImGui.TableNextColumn();
var count = entry.FileGamePaths.Concat(entry.FileSwaps).Count();
ImGui.TextUnformatted(count.ToString());
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
UiSharedService.AttachToolTip(count == 0 ? "No File data attached" : "Has File data attached");
ImGui.TableNextColumn();
bool hasGlamourerData = !string.IsNullOrEmpty(entry.GlamourerData);
_uiSharedService.BooleanToColoredIcon(hasGlamourerData, false);
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
UiSharedService.AttachToolTip(string.IsNullOrEmpty(entry.GlamourerData) ? "No Glamourer data attached" : "Has Glamourer data attached");
ImGui.TableNextColumn();
bool hasCustomizeData = !string.IsNullOrEmpty(entry.CustomizeData);
_uiSharedService.BooleanToColoredIcon(hasCustomizeData, false);
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
UiSharedService.AttachToolTip(string.IsNullOrEmpty(entry.CustomizeData) ? "No Customize+ data attached" : "Has Customize+ data attached");
ImGui.TableNextColumn();
FontAwesomeIcon eIcon = FontAwesomeIcon.None;
if (!Equals(DateTime.MaxValue, entry.ExpiryDate))
eIcon = FontAwesomeIcon.Clock;
_uiSharedService.IconText(eIcon, ImGuiColors.DalamudYellow);
if (ImGui.IsItemClicked()) _selectedDtoId = entry.Id;
if (eIcon != FontAwesomeIcon.None)
{
UiSharedService.AttachToolTip($"This entry will expire on {entry.ExpiryDate.ToLocalTime()}");
}
}
}
}
using (ImRaii.Disabled(!_charaDataManager.Initialized || _charaDataManager.DataCreationTask != null || _charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "New Character Data Entry"))
{
_charaDataManager.CreateCharaDataEntry(_closalCts.Token);
}
}
if (_charaDataManager.DataCreationTask != null)
{
UiSharedService.AttachToolTip("You can only create new character data every few seconds. Please wait.");
}
if (!_charaDataManager.Initialized)
{
UiSharedService.AttachToolTip("Please use the button \"Get Own Chara Data\" once before you can add new data entries.");
}
if (_charaDataManager.Initialized)
{
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
UiSharedService.TextWrapped($"Chara Data Entries on Server: {_charaDataManager.OwnCharaData.Count}/{_charaDataManager.MaxCreatableCharaData}");
if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)
{
ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", ImGuiColors.DalamudYellow);
}
}
if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted)
{
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", ImGuiColors.DalamudYellow);
}
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
{
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color);
}
ImGuiHelpers.ScaledDummy(10);
ImGui.Separator();
_ = _charaDataManager.OwnCharaData.TryGetValue(_selectedDtoId, out var dto);
DrawEditCharaData(dto);
}
private void DrawSpecificIndividuals(CharaDataExtendedUpdateDto updateDto)
{
UiSharedService.DrawTree("Access for Specific Individuals", () =>
{
ImGui.SetNextItemWidth(200);
ImGui.InputText("##AliasToAdd", ref _specificIndividualAdd, 20);
ImGui.SameLine();
using (ImRaii.Disabled(string.IsNullOrEmpty(_specificIndividualAdd)
|| updateDto.UserList.Any(f => string.Equals(f.UID, _specificIndividualAdd, StringComparison.Ordinal) || string.Equals(f.Alias, _specificIndividualAdd, StringComparison.Ordinal))))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
updateDto.AddToList(_specificIndividualAdd);
_specificIndividualAdd = string.Empty;
}
}
ImGui.SameLine();
ImGui.TextUnformatted("UID/Vanity ID to Add");
_uiSharedService.DrawHelpText("Users added to this list will be able to access this character data regardless of your pause or pair state with them." + UiSharedService.TooltipSeparator
+ "Note: Mistyped entries will be automatically removed on updating data to server.");
using (var lb = ImRaii.ListBox("Allowed Individuals", new(200, 200)))
{
foreach (var user in updateDto.UserList)
{
var userString = string.IsNullOrEmpty(user.Alias) ? user.UID : $"{user.Alias} ({user.UID})";
if (ImGui.Selectable(userString, string.Equals(user.UID, _selectedSpecificIndividual, StringComparison.Ordinal)))
{
_selectedSpecificIndividual = user.UID;
}
}
}
using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedSpecificIndividual)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove selected User"))
{
updateDto.RemoveFromList(_selectedSpecificIndividual);
_selectedSpecificIndividual = string.Empty;
}
}
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
});
}
}

View File

@@ -0,0 +1,199 @@
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Utility;
using Dalamud.Interface;
using ImGuiNET;
using System.Numerics;
namespace MareSynchronos.UI;
internal partial class CharaDataHubUi
{
private void DrawNearbyPoses()
{
_uiSharedService.BigText("Poses Nearby");
DrawHelpFoldout("This tab will show you all Shared World Poses nearby you." + Environment.NewLine + Environment.NewLine
+ "Shared World Poses are poses in character data that have world data attached to them and are set to shared. "
+ "This means that all data that is in 'Shared with You' that has a pose with world data attached to it will be shown here if you are nearby." + Environment.NewLine
+ "By default all poses that are shared will be shown. Poses taken in housing areas will by default only be shown on the correct server and location." + Environment.NewLine + Environment.NewLine
+ "Shared World Poses will appear in the world as floating wisps, as well as in the list below. You can mouse over a Shared World Pose in the list for it to get highlighted in the world." + Environment.NewLine + Environment.NewLine
+ "You can apply Shared World Poses to yourself or spawn the associated character to pose with them." + Environment.NewLine + Environment.NewLine
+ "You can adjust the filter and change further settings in the 'Settings & Filter' foldout.");
UiSharedService.DrawTree("Settings & Filters", () =>
{
string filterByUser = _charaDataNearbyManager.UserNoteFilter;
if (ImGui.InputTextWithHint("##filterbyuser", "Filter by User", ref filterByUser, 50))
{
_charaDataNearbyManager.UserNoteFilter = filterByUser;
}
bool onlyCurrent = _configService.Current.NearbyOwnServerOnly;
if (ImGui.Checkbox("Only show Poses on current server", ref onlyCurrent))
{
_configService.Current.NearbyOwnServerOnly = onlyCurrent;
_configService.Save();
}
_uiSharedService.DrawHelpText("Toggling this off will show you the location of all shared Poses with World Data from all Servers");
bool showOwn = _configService.Current.NearbyShowOwnData;
if (ImGui.Checkbox("Also show your own data", ref showOwn))
{
_configService.Current.NearbyShowOwnData = showOwn;
_configService.Save();
}
_uiSharedService.DrawHelpText("Toggling this on will also show you the location of your own Poses");
bool ignoreHousing = _configService.Current.NearbyIgnoreHousingLimitations;
if (ImGui.Checkbox("Ignore Housing Limitations", ref ignoreHousing))
{
_configService.Current.NearbyIgnoreHousingLimitations = ignoreHousing;
_configService.Save();
}
_uiSharedService.DrawHelpText("This will display all poses in their location regardless of housing limitations. (Ignoring Ward, Plot, Room)" + UiSharedService.TooltipSeparator
+ "Note: Poses that utilize housing props, furniture, etc. will not be displayed correctly if not spawned in the right location.");
bool showWisps = _configService.Current.NearbyDrawWisps;
if (ImGui.Checkbox("Show Pose Wisps in the overworld", ref showWisps))
{
_configService.Current.NearbyDrawWisps = showWisps;
_configService.Save();
}
_uiSharedService.DrawHelpText("When enabled, Mare will draw floating wisps where other's poses are in the world.");
int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
ImGui.SetNextItemWidth(100);
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))
{
_configService.Current.NearbyDistanceFilter = poseDetectionDistance;
_configService.Save();
}
_uiSharedService.DrawHelpText("This setting allows you to change the maximum distance in which poses will be shown. Set it to the maximum if you want to see all poses on the current map.");
});
if (!_uiSharedService.IsInGpose)
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", ImGuiColors.DalamudYellow);
ImGuiHelpers.ScaledDummy(5);
}
DrawUpdateSharedDataButton();
UiSharedService.DistanceSeparator();
using var child = ImRaii.Child("nearbyPosesChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize);
ImGuiHelpers.ScaledDummy(3f);
using var indent = ImRaii.PushIndent(5f);
if (_charaDataNearbyManager.NearbyData.Count == 0)
{
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", ImGuiColors.DalamudYellow);
}
bool wasAnythingHovered = false;
int i = 0;
foreach (var pose in _charaDataNearbyManager.NearbyData.OrderBy(v => v.Value.Distance))
{
using var poseId = ImRaii.PushId("nearbyPose" + (i++));
var pos = ImGui.GetCursorPos();
var circleDiameter = 60f;
var circleOriginX = ImGui.GetWindowContentRegionMax().X - circleDiameter - pos.X;
float circleOffsetY = 0;
UiSharedService.DrawGrouped(() =>
{
string? userNote = _serverConfigurationManager.GetNoteForUid(pose.Key.MetaInfo.Uploader.UID);
var noteText = pose.Key.MetaInfo.IsOwnData ? "YOU" : (userNote == null ? pose.Key.MetaInfo.Uploader.AliasOrUID : $"{userNote} ({pose.Key.MetaInfo.Uploader.AliasOrUID})");
ImGui.TextUnformatted("Pose by");
ImGui.SameLine();
UiSharedService.ColorText(noteText, ImGuiColors.ParsedGreen);
using (ImRaii.Group())
{
UiSharedService.ColorText("Character Data Description", ImGuiColors.DalamudGrey);
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.ExternalLinkAlt, ImGuiColors.DalamudGrey);
}
UiSharedService.AttachToolTip(pose.Key.MetaInfo.Description);
UiSharedService.ColorText("Description", ImGuiColors.DalamudGrey);
ImGui.SameLine();
UiSharedService.TextWrapped(pose.Key.Description ?? "No Pose Description was set", circleOriginX);
var posAfterGroup = ImGui.GetCursorPos();
var groupHeightCenter = (posAfterGroup.Y - pos.Y) / 2;
circleOffsetY = (groupHeightCenter - circleDiameter / 2);
if (circleOffsetY < 0) circleOffsetY = 0;
ImGui.SetCursorPos(new Vector2(circleOriginX, pos.Y));
ImGui.Dummy(new Vector2(circleDiameter, circleDiameter));
UiSharedService.AttachToolTip("Click to open corresponding map and set map marker" + UiSharedService.TooltipSeparator
+ pose.Key.WorldDataDescriptor);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
_dalamudUtilService.SetMarkerAndOpenMap(pose.Key.Position, pose.Key.Map);
}
ImGui.SetCursorPos(posAfterGroup);
if (_uiSharedService.IsInGpose)
{
GposePoseAction(() =>
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply Pose"))
{
_charaDataManager.ApplyFullPoseDataToGposeTarget(pose.Key);
}
}, $"Apply pose and position to {CharaName(_gposeTarget)}", _hasValidGposeTarget);
ImGui.SameLine();
GposeMetaInfoAction((_) =>
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Spawn and Pose"))
{
_charaDataManager.SpawnAndApplyWorldTransform(pose.Key.MetaInfo, pose.Key);
}
}, "Spawn actor and apply pose and position", pose.Key.MetaInfo, _hasValidGposeTarget, true);
}
});
if (ImGui.IsItemHovered())
{
wasAnythingHovered = true;
_nearbyHovered = pose.Key;
}
var drawList = ImGui.GetWindowDrawList();
var circleRadius = circleDiameter / 2f;
var windowPos = ImGui.GetWindowPos();
var scrollX = ImGui.GetScrollX();
var scrollY = ImGui.GetScrollY();
var circleCenter = new Vector2(windowPos.X + circleOriginX + circleRadius - scrollX, windowPos.Y + pos.Y + circleRadius + circleOffsetY - scrollY);
var rads = pose.Value.Direction * (Math.PI / 180);
float halfConeAngleRadians = 15f * (float)Math.PI / 180f;
Vector2 baseDir1 = new Vector2((float)Math.Sin(rads - halfConeAngleRadians), -(float)Math.Cos(rads - halfConeAngleRadians));
Vector2 baseDir2 = new Vector2((float)Math.Sin(rads + halfConeAngleRadians), -(float)Math.Cos(rads + halfConeAngleRadians));
Vector2 coneBase1 = circleCenter + baseDir1 * circleRadius;
Vector2 coneBase2 = circleCenter + baseDir2 * circleRadius;
// Draw the cone as a filled triangle
drawList.AddTriangleFilled(circleCenter, coneBase1, coneBase2, UiSharedService.Color(ImGuiColors.ParsedGreen));
drawList.AddCircle(circleCenter, circleDiameter / 2, UiSharedService.Color(ImGuiColors.DalamudWhite), 360, 2);
var distance = pose.Value.Distance.ToString("0.0") + "y";
var textSize = ImGui.CalcTextSize(distance);
drawList.AddText(new Vector2(circleCenter.X - textSize.X / 2, circleCenter.Y + textSize.Y / 3f), UiSharedService.Color(ImGuiColors.DalamudWhite), distance);
ImGuiHelpers.ScaledDummy(3);
}
if (!wasAnythingHovered) _nearbyHovered = null;
_charaDataNearbyManager.SetHoveredVfx(_nearbyHovered);
}
private void DrawUpdateSharedDataButton()
{
using (ImRaii.Disabled(_charaDataManager.GetAllDataTask != null
|| (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted)))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Update Data Shared With You"))
{
_ = _charaDataManager.GetAllSharedData(_disposalCts.Token).ContinueWith(u => UpdateFilteredItems());
}
}
if (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted)
{
UiSharedService.AttachToolTip("You can only refresh all character data from server every minute. Please wait.");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,100 +0,0 @@
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiFileDialog;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Export;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.UI;
public class GposeUi : WindowMediatorSubscriberBase
{
private readonly MareConfigService _configService;
private readonly UiSharedService _uiSharedService;
private readonly DalamudUtilService _dalamudUtil;
private readonly FileDialogManager _fileDialogManager;
private readonly MareCharaFileManager _mareCharaFileManager;
private Task<long>? _expectedLength;
private Task? _applicationTask;
public GposeUi(ILogger<GposeUi> logger, MareCharaFileManager mareCharaFileManager,
DalamudUtilService dalamudUtil, FileDialogManager fileDialogManager, MareConfigService configService,
MareMediator mediator, PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService)
: base(logger, mediator, "Loporrit Gpose Import UI###LoporritSyncGposeUI", performanceCollectorService)
{
_mareCharaFileManager = mareCharaFileManager;
_dalamudUtil = dalamudUtil;
_fileDialogManager = fileDialogManager;
_configService = configService;
_uiSharedService = uiSharedService;
Mediator.Subscribe<GposeStartMessage>(this, (_) => StartGpose());
Mediator.Subscribe<GposeEndMessage>(this, (_) => EndGpose());
IsOpen = _dalamudUtil.IsInGpose;
this.SizeConstraints = new()
{
MinimumSize = new(200, 200),
MaximumSize = new(400, 400)
};
}
protected override void DrawInternal()
{
if (!_dalamudUtil.IsInGpose) IsOpen = false;
if (!_mareCharaFileManager.CurrentlyWorking)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FolderOpen, "Load MCDF"))
{
_fileDialogManager.OpenFileDialog("Pick MCDF file", ".mcdf", (success, paths) =>
{
if (!success) return;
if (paths.FirstOrDefault() is not string path) return;
_configService.Current.ExportFolder = Path.GetDirectoryName(path) ?? string.Empty;
_configService.Save();
_expectedLength = Task.Run(() => _mareCharaFileManager.LoadMareCharaFile(path));
}, 1, Directory.Exists(_configService.Current.ExportFolder) ? _configService.Current.ExportFolder : null);
}
UiSharedService.AttachToolTip("Applies it to the currently selected GPose actor");
if (_mareCharaFileManager.LoadedCharaFile != null && _expectedLength != null)
{
UiSharedService.TextWrapped("Loaded file: " + _mareCharaFileManager.LoadedCharaFile.FilePath);
UiSharedService.TextWrapped("File Description: " + _mareCharaFileManager.LoadedCharaFile.CharaFileData.Description);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Apply loaded MCDF"))
{
_applicationTask = _dalamudUtil.RunOnFrameworkThread(async () => await _mareCharaFileManager.ApplyMareCharaFile(_dalamudUtil.GposeTargetGameObject, _expectedLength!.GetAwaiter().GetResult()).ConfigureAwait(false));
}
UiSharedService.AttachToolTip("Applies it to the currently selected GPose actor");
UiSharedService.ColorTextWrapped("Warning: redrawing or changing the character will revert all applied mods.", ImGuiColors.DalamudYellow);
}
if (_applicationTask?.IsFaulted ?? false)
{
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
ImGuiColors.DalamudRed);
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
"If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow);
}
}
else
{
UiSharedService.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow);
}
UiSharedService.TextWrapped("Hint: You can disable the automatic loading of this window in the Loporrit settings and open it manually with /sync gpose");
}
private void EndGpose()
{
IsOpen = false;
_applicationTask = null;
_expectedLength = null;
_mareCharaFileManager.ClearMareCharaFile();
}
private void StartGpose()
{
IsOpen = _configService.Current.OpenGposeImportOnGposeStart;
}
}

View File

@@ -13,7 +13,6 @@ using MareSynchronos.FileCache;
using MareSynchronos.Interop.Ipc; using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models; using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Export;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
@@ -47,7 +46,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly FileUploadManager _fileTransferManager; private readonly FileUploadManager _fileTransferManager;
private readonly FileTransferOrchestrator _fileTransferOrchestrator; private readonly FileTransferOrchestrator _fileTransferOrchestrator;
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly MareCharaFileManager _mareCharaFileManager;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly ChatService _chatService; private readonly ChatService _chatService;
private readonly GuiHookService _guiHookService; private readonly GuiHookService _guiHookService;
@@ -58,16 +56,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
private bool _deleteAccountPopupModalShown = false; private bool _deleteAccountPopupModalShown = false;
private bool _deleteFilesPopupModalShown = false; private bool _deleteFilesPopupModalShown = false;
private string _exportDescription = string.Empty;
private string _lastTab = string.Empty; private string _lastTab = string.Empty;
private bool? _notesSuccessfullyApplied = null; private bool? _notesSuccessfullyApplied = null;
private bool _overwriteExistingLabels = false; private bool _overwriteExistingLabels = false;
private bool _readClearCache = false; private bool _readClearCache = false;
private bool _readExport = false; private CancellationTokenSource? _validationCts;
private Task<List<FileCacheEntity>>? _validationTask;
private bool _wasOpen = false; private bool _wasOpen = false;
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress; private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
private Task<List<FileCacheEntity>>? _validationTask;
private CancellationTokenSource? _validationCts;
private (int, int, FileCacheEntity) _currentProgress; private (int, int, FileCacheEntity) _currentProgress;
private Task? _exportTask; private Task? _exportTask;
@@ -77,7 +73,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
public SettingsUi(ILogger<SettingsUi> logger, public SettingsUi(ILogger<SettingsUi> logger,
UiSharedService uiShared, MareConfigService configService, UiSharedService uiShared, MareConfigService configService,
MareCharaFileManager mareCharaFileManager, PairManager pairManager, ChatService chatService, GuiHookService guiHookService, PairManager pairManager, ChatService chatService, GuiHookService guiHookService,
ServerConfigurationManager serverConfigurationManager, ServerConfigurationManager serverConfigurationManager,
PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceService playerPerformanceService, PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceService playerPerformanceService,
MareMediator mediator, PerformanceCollectorService performanceCollector, MareMediator mediator, PerformanceCollectorService performanceCollector,
@@ -89,7 +85,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
DalamudUtilService dalamudUtilService) : base(logger, mediator, "Loporrit Settings", performanceCollector) DalamudUtilService dalamudUtilService) : base(logger, mediator, "Loporrit Settings", performanceCollector)
{ {
_configService = configService; _configService = configService;
_mareCharaFileManager = mareCharaFileManager;
_pairManager = pairManager; _pairManager = pairManager;
_chatService = chatService; _chatService = chatService;
_guiHookService = guiHookService; _guiHookService = guiHookService;
@@ -742,62 +737,18 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.BigText("Export MCDF"); _uiShared.BigText("Export MCDF");
UiSharedService.TextWrapped("This feature allows you to pack your character appearance into a MCDF file and manually send it to other people. MCDF files can imported during GPose."); ImGuiHelpers.ScaledDummy(10);
ImGui.Checkbox("##readExport", ref _readExport); UiSharedService.ColorTextWrapped("Exporting MCDF has moved.", ImGuiColors.DalamudYellow);
ImGui.SameLine(); ImGuiHelpers.ScaledDummy(5);
UiSharedService.TextWrapped("I understand that by exporting my character data and sending it to other people I am giving away my current character appearance irrevocably. People I am sharing my data with have the ability to share it with other people without limitations."); UiSharedService.TextWrapped("It is now found in the Main UI under \"Character Data Hub\"");
if (_uiShared.IconTextButton(FontAwesomeIcon.Running, "Open Character Data Hub"))
if (_readExport)
{ {
ImGui.Indent(); Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
if (!_mareCharaFileManager.CurrentlyWorking)
{
ImGui.InputTextWithHint("Export Descriptor", "This description will be shown on loading the data", ref _exportDescription, 255);
if (_uiShared.IconTextButton(FontAwesomeIcon.Save, "Export Character as MCDF"))
{
string defaultFileName = string.IsNullOrEmpty(_exportDescription)
? "export.mcdf"
: string.Join('_', $"{_exportDescription}.mcdf".Split(Path.GetInvalidFileNameChars()));
_uiShared.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", defaultFileName, ".mcdf", (success, path) =>
{
if (!success) return;
_configService.Current.ExportFolder = Path.GetDirectoryName(path) ?? string.Empty;
_configService.Save();
_exportTask = Task.Run(() =>
{
var desc = _exportDescription;
_exportDescription = string.Empty;
_mareCharaFileManager.SaveMareCharaFile(LastCreatedCharacterData, desc, path);
});
}, Directory.Exists(_configService.Current.ExportFolder) ? _configService.Current.ExportFolder : null);
}
UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" +
" equipped and redraw your character before exporting.", ImGuiColors.DalamudYellow);
}
else
{
UiSharedService.ColorTextWrapped("Export in progress", ImGuiColors.DalamudYellow);
} }
if (_exportTask?.IsFaulted ?? false) UiSharedService.TextWrapped("Note: this entry will be removed in the near future. Please use the Main UI to open the Character Data Hub.");
{ ImGuiHelpers.ScaledDummy(5);
UiSharedService.ColorTextWrapped("Export failed, check /xllog for more details.", ImGuiColors.DalamudRed);
}
ImGui.Unindent();
}
bool openInGpose = _configService.Current.OpenGposeImportOnGposeStart;
if (ImGui.Checkbox("Open MCDF import window when GPose loads", ref openInGpose))
{
_configService.Current.OpenGposeImportOnGposeStart = openInGpose;
_configService.Save();
}
_uiShared.DrawHelpText("This will automatically open the import menu when loading into Gpose. If unchecked you can open the menu manually with /sync gpose");
ImGui.Separator(); ImGui.Separator();
_uiShared.BigText("Storage"); _uiShared.BigText("Storage");
@@ -1958,7 +1909,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
if (ImGui.BeginTabItem("Export & Storage")) if (ImGui.BeginTabItem("Storage"))
{ {
DrawFileStorageSettings(); DrawFileStorageSettings();
ImGui.EndTabItem(); ImGui.EndTabItem();

View File

@@ -78,6 +78,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
private bool _moodlesExists = false; private bool _moodlesExists = false;
private bool _penumbraExists = false; private bool _penumbraExists = false;
private bool _petNamesExists = false; private bool _petNamesExists = false;
private bool _brioExists = false;
private int _serverSelectionIndex = -1; private int _serverSelectionIndex = -1;
@@ -111,6 +112,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
_honorificExists = _ipcManager.Honorific.APIAvailable; _honorificExists = _ipcManager.Honorific.APIAvailable;
_petNamesExists = _ipcManager.PetNames.APIAvailable; _petNamesExists = _ipcManager.PetNames.APIAvailable;
_moodlesExists = _ipcManager.Moodles.APIAvailable; _moodlesExists = _ipcManager.Moodles.APIAvailable;
_brioExists = _ipcManager.Brio.APIAvailable;
}); });
UidFont = _pluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e => UidFont = _pluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
@@ -133,7 +135,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
public bool HasValidPenumbraModPath => !(_ipcManager.Penumbra.ModDirectory ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.Penumbra.ModDirectory); public bool HasValidPenumbraModPath => !(_ipcManager.Penumbra.ModDirectory ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.Penumbra.ModDirectory);
public IFontHandle IconFont { get; init; } public IFontHandle IconFont { get; init; }
public bool IsInGpose => _dalamudUtil.IsInCutscene; public bool IsInGpose => _dalamudUtil.IsInGpose;
public string PlayerName => _dalamudUtil.GetPlayerName(); public string PlayerName => _dalamudUtil.GetPlayerName();
@@ -207,14 +209,46 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
ImGui.TextUnformatted(text); ImGui.TextUnformatted(text);
} }
public static void ColorTextWrapped(string text, Vector4 color) public static void ColorTextWrapped(string text, Vector4 color, float wrapPos = 0)
{ {
using var raiicolor = ImRaii.PushColor(ImGuiCol.Text, color); using var raiicolor = ImRaii.PushColor(ImGuiCol.Text, color);
TextWrapped(text); TextWrapped(text, wrapPos);
} }
public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0; public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0;
public static void DrawGrouped(Action imguiDrawAction, float rounding = 5f, float? expectedWidth = null)
{
var cursorPos = ImGui.GetCursorPos();
using (ImRaii.Group())
{
if (expectedWidth != null)
{
ImGui.Dummy(new(expectedWidth.Value, 0));
ImGui.SetCursorPos(cursorPos);
}
imguiDrawAction.Invoke();
}
ImGui.GetWindowDrawList().AddRect(
ImGui.GetItemRectMin() - ImGui.GetStyle().ItemInnerSpacing,
ImGui.GetItemRectMax() + ImGui.GetStyle().ItemInnerSpacing,
Color(ImGuiColors.DalamudGrey2), rounding);
}
public static void DrawGroupedCenteredColorText(string text, Vector4 color, float? maxWidth = null)
{
var availWidth = ImGui.GetContentRegionAvail().X;
var textWidth = ImGui.CalcTextSize(text, availWidth).X;
if (maxWidth != null && textWidth > maxWidth * ImGuiHelpers.GlobalScale) textWidth = maxWidth.Value * ImGuiHelpers.GlobalScale;
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (availWidth / 2f) - (textWidth / 2f));
DrawGrouped(() =>
{
ColorTextWrapped(text, color, ImGui.GetCursorPosX() + textWidth);
}, expectedWidth: maxWidth == null ? null : maxWidth * ImGuiHelpers.GlobalScale);
}
public static void DrawOutlinedFont(string text, Vector4 fontColor, Vector4 outlineColor, int thickness) public static void DrawOutlinedFont(string text, Vector4 fontColor, Vector4 outlineColor, int thickness)
{ {
var original = ImGui.GetCursorPos(); var original = ImGui.GetCursorPos();
@@ -271,6 +305,15 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
drawList.AddText(textPos, fontColor, text); drawList.AddText(textPos, fontColor, text);
} }
public static void DrawTree(string leafName, Action drawOnOpened, ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.None)
{
using var tree = ImRaii.TreeNode(leafName, flags);
if (tree)
{
drawOnOpened();
}
}
public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
public float GetIconTextButtonSize(FontAwesomeIcon icon, string text) public float GetIconTextButtonSize(FontAwesomeIcon icon, string text)
@@ -428,9 +471,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
public static bool ShiftPressed() => (GetKeyState(0xA1) & 0x8000) != 0 || (GetKeyState(0xA0) & 0x8000) != 0; public static bool ShiftPressed() => (GetKeyState(0xA1) & 0x8000) != 0 || (GetKeyState(0xA0) & 0x8000) != 0;
public static void TextWrapped(string text) public static void TextWrapped(string text, float wrapPos = 0)
{ {
ImGui.PushTextWrapPos(0); ImGui.PushTextWrapPos(wrapPos);
ImGui.TextUnformatted(text); ImGui.TextUnformatted(text);
ImGui.PopTextWrapPos(); ImGui.PopTextWrapPos();
} }
@@ -739,15 +782,10 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
IconText(_penumbraExists ? check : cross, GetBoolColor(_penumbraExists)); IconText(_penumbraExists ? check : cross, GetBoolColor(_penumbraExists));
ImGui.SameLine(); ImGui.SameLine();
AttachToolTip($"Penumbra is " + (_penumbraExists ? "available and up to date." : "unavailable or not up to date.")); AttachToolTip($"Penumbra is " + (_penumbraExists ? "available and up to date." : "unavailable or not up to date."));
ImGui.Spacing();
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted("Glamourer"); ColorText("Glamourer", GetBoolColor(_glamourerExists));
ImGui.SameLine();
IconText(_glamourerExists ? check : cross, GetBoolColor(_glamourerExists));
ImGui.SameLine();
AttachToolTip($"Glamourer is " + (_glamourerExists ? "available and up to date." : "unavailable or not up to date.")); AttachToolTip($"Glamourer is " + (_glamourerExists ? "available and up to date." : "unavailable or not up to date."));
ImGui.Spacing();
if (intro) if (intro)
{ {
@@ -803,6 +841,14 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
AttachToolTip($"Moodles is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date.")); AttachToolTip($"Moodles is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date."));
ImGui.Spacing(); ImGui.Spacing();
ImGui.SameLine();
ImGui.TextUnformatted("Brio");
ImGui.SameLine();
IconText(_brioExists ? check : cross, GetBoolColor(_brioExists));
ImGui.SameLine();
AttachToolTip($"Brio is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date."));
ImGui.Spacing();
if (!_penumbraExists || !_glamourerExists) if (!_penumbraExists || !_glamourerExists)
{ {
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Loporrit."); ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Loporrit.");
@@ -923,6 +969,13 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
Strings.ToS = new Strings.ToSStrings(); Strings.ToS = new Strings.ToSStrings();
} }
internal static void DistanceSeparator()
{
ImGuiHelpers.ScaledDummy(5);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
}
[LibraryImport("user32")] [LibraryImport("user32")]
internal static partial short GetKeyState(int nVirtKey); internal static partial short GetKeyState(int nVirtKey);

View File

@@ -0,0 +1,22 @@
namespace MareSynchronos.Utils;
public class ValueProgress<T> : Progress<T>
{
public T? Value { get; set; }
protected override void OnReport(T value)
{
base.OnReport(value);
Value = value;
}
public void Report(T value)
{
OnReport(value);
}
public void Clear()
{
Value = default;
}
}

View File

@@ -65,6 +65,43 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesDeleteAllFullPath(_orchestrator.FilesCdnUri!)).ConfigureAwait(false); await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesDeleteAllFullPath(_orchestrator.FilesCdnUri!)).ConfigureAwait(false);
} }
public async Task<List<string>> UploadFiles(List<string> hashesToUpload, IProgress<string> progress, CancellationToken? ct = null)
{
Logger.LogDebug("Trying to upload files");
var filesPresentLocally = hashesToUpload.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal);
var locallyMissingFiles = hashesToUpload.Except(filesPresentLocally, StringComparer.Ordinal).ToList();
if (locallyMissingFiles.Any())
{
return locallyMissingFiles;
}
progress.Report($"Starting upload for {filesPresentLocally.Count} files");
var filesToUpload = await FilesSend([.. filesPresentLocally], [], ct ?? CancellationToken.None).ConfigureAwait(false);
if (filesToUpload.Exists(f => f.IsForbidden))
{
return [.. filesToUpload.Where(f => f.IsForbidden).Select(f => f.Hash)];
}
Task uploadTask = Task.CompletedTask;
int i = 1;
foreach (var file in filesToUpload)
{
progress.Report($"Uploading file {i++}/{filesToUpload.Count}. Please wait until the upload is completed.");
Logger.LogDebug("[{hash}] Compressing", file);
var data = await _fileDbManager.GetCompressedFileData(file.Hash, ct ?? CancellationToken.None).ConfigureAwait(false);
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, _fileDbManager.GetFileCacheByHash(data.Item1)!.ResolvedFilepath);
await uploadTask.ConfigureAwait(false);
uploadTask = UploadFile(data.Item2, file.Hash, false, ct ?? CancellationToken.None);
(ct ?? CancellationToken.None).ThrowIfCancellationRequested();
}
await uploadTask.ConfigureAwait(false);
return [];
}
public async Task<CharacterData> UploadFiles(CharacterData data, List<UserData> visiblePlayers) public async Task<CharacterData> UploadFiles(CharacterData data, List<UserData> visiblePlayers)
{ {
CancelUpload(); CancelUpload();
@@ -135,7 +172,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
_verifiedUploadedHashes.Clear(); _verifiedUploadedHashes.Clear();
} }
private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) private async Task UploadFile(byte[] compressedFile, string fileHash, bool postProgress, CancellationToken uploadToken)
{ {
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized"); if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
@@ -145,7 +182,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
try try
{ {
await UploadFileStream(compressedFile, fileHash, false, uploadToken).ConfigureAwait(false); await UploadFileStream(compressedFile, fileHash, false, postProgress, uploadToken).ConfigureAwait(false);
_verifiedUploadedHashes[fileHash] = DateTime.UtcNow; _verifiedUploadedHashes[fileHash] = DateTime.UtcNow;
} }
catch (Exception ex) catch (Exception ex)
@@ -153,7 +190,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
if (false && ex is not OperationCanceledException) if (false && ex is not OperationCanceledException)
{ {
Logger.LogWarning(ex, "[{hash}] Error during file upload, trying alternative file upload", fileHash); Logger.LogWarning(ex, "[{hash}] Error during file upload, trying alternative file upload", fileHash);
await UploadFileStream(compressedFile, fileHash, munged: true, uploadToken).ConfigureAwait(false); await UploadFileStream(compressedFile, fileHash, munged: true, postProgress, uploadToken).ConfigureAwait(false);
} }
else else
{ {
@@ -162,14 +199,14 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
} }
} }
private async Task UploadFileStream(byte[] compressedFile, string fileHash, bool munged, CancellationToken uploadToken) private async Task UploadFileStream(byte[] compressedFile, string fileHash, bool munged, bool postProgress, CancellationToken uploadToken)
{ {
if (munged) if (munged)
throw new NotImplementedException(); throw new NotImplementedException();
using var ms = new MemoryStream(compressedFile); using var ms = new MemoryStream(compressedFile);
Progress<UploadProgress> prog = new((prog) => Progress<UploadProgress>? prog = !postProgress ? null : new((prog) =>
{ {
try try
{ {
@@ -180,6 +217,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
Logger.LogWarning(ex, "[{hash}] Could not set upload progress", fileHash); Logger.LogWarning(ex, "[{hash}] Could not set upload progress", fileHash);
} }
}); });
var streamContent = new ProgressableStreamContent(ms, prog); var streamContent = new ProgressableStreamContent(ms, prog);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
HttpResponseMessage response; HttpResponseMessage response;
@@ -235,7 +273,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
CurrentUploads.Single(e => string.Equals(e.Hash, file.Hash, StringComparison.Ordinal)).Total = data.Item2.Length; CurrentUploads.Single(e => string.Equals(e.Hash, file.Hash, StringComparison.Ordinal)).Total = data.Item2.Length;
Logger.LogDebug("[{hash}] Starting upload for {filePath}", file.Hash, _fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath); Logger.LogDebug("[{hash}] Starting upload for {filePath}", file.Hash, _fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath);
await uploadTask.ConfigureAwait(false); await uploadTask.ConfigureAwait(false);
uploadTask = UploadFile(data.Item2, file.Hash, uploadToken); uploadTask = UploadFile(data.Item2, file.Hash, true, uploadToken);
uploadToken.ThrowIfCancellationRequested(); uploadToken.ThrowIfCancellationRequested();
} }

View File

@@ -6,16 +6,16 @@ public class ProgressableStreamContent : StreamContent
{ {
private const int _defaultBufferSize = 4096; private const int _defaultBufferSize = 4096;
private readonly int _bufferSize; private readonly int _bufferSize;
private readonly IProgress<UploadProgress> _progress; private readonly IProgress<UploadProgress>? _progress;
private readonly Stream _streamToWrite; private readonly Stream _streamToWrite;
private bool _contentConsumed; private bool _contentConsumed;
public ProgressableStreamContent(Stream streamToWrite, IProgress<UploadProgress> downloader) public ProgressableStreamContent(Stream streamToWrite, IProgress<UploadProgress>? downloader)
: this(streamToWrite, _defaultBufferSize, downloader) : this(streamToWrite, _defaultBufferSize, downloader)
{ {
} }
public ProgressableStreamContent(Stream streamToWrite, int bufferSize, IProgress<UploadProgress> progress) public ProgressableStreamContent(Stream streamToWrite, int bufferSize, IProgress<UploadProgress>? progress)
: base(streamToWrite, bufferSize) : base(streamToWrite, bufferSize)
{ {
if (streamToWrite == null) if (streamToWrite == null)
@@ -55,14 +55,14 @@ public class ProgressableStreamContent : StreamContent
{ {
while (true) while (true)
{ {
var length = _streamToWrite.Read(buffer, 0, buffer.Length); var length = await _streamToWrite.ReadAsync(buffer).ConfigureAwait(false);
if (length <= 0) if (length <= 0)
{ {
break; break;
} }
uploaded += length; uploaded += length;
_progress.Report(new UploadProgress(uploaded, size)); _progress?.Report(new UploadProgress(uploaded, size));
await stream.WriteAsync(buffer.AsMemory(0, length)).ConfigureAwait(false); await stream.WriteAsync(buffer.AsMemory(0, length)).ConfigureAwait(false);
} }
} }

View File

@@ -0,0 +1,119 @@
using MareSynchronos.API.Dto.CharaData;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.WebAPI;
public partial class ApiController
{
public async Task<CharaDataFullDto?> CharaDataCreate()
{
if (!IsConnected) return null;
try
{
Logger.LogDebug("Creating new Character Data");
return await _mareHub!.InvokeAsync<CharaDataFullDto?>(nameof(CharaDataCreate)).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to create new character data");
return null;
}
}
public async Task<CharaDataFullDto?> CharaDataUpdate(CharaDataUpdateDto updateDto)
{
if (!IsConnected) return null;
try
{
Logger.LogDebug("Updating chara data for {id}", updateDto.Id);
return await _mareHub!.InvokeAsync<CharaDataFullDto?>(nameof(CharaDataUpdate), updateDto).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to update chara data for {id}", updateDto.Id);
return null;
}
}
public async Task<bool> CharaDataDelete(string id)
{
if (!IsConnected) return false;
try
{
Logger.LogDebug("Deleting chara data for {id}", id);
return await _mareHub!.InvokeAsync<bool>(nameof(CharaDataDelete), id).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to delete chara data for {id}", id);
return false;
}
}
public async Task<CharaDataMetaInfoDto?> CharaDataGetMetainfo(string id)
{
if (!IsConnected) return null;
try
{
Logger.LogDebug("Getting metainfo for chara data {id}", id);
return await _mareHub!.InvokeAsync<CharaDataMetaInfoDto?>(nameof(CharaDataGetMetainfo), id).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to get meta info for chara data {id}", id);
return null;
}
}
public async Task<List<CharaDataFullDto>> CharaDataGetOwn()
{
if (!IsConnected) return [];
try
{
Logger.LogDebug("Getting all own chara data");
return await _mareHub!.InvokeAsync<List<CharaDataFullDto>>(nameof(CharaDataGetOwn)).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to get own chara data");
return [];
}
}
public async Task<List<CharaDataMetaInfoDto>> CharaDataGetShared()
{
if (!IsConnected) return [];
try
{
Logger.LogDebug("Getting all own chara data");
return await _mareHub!.InvokeAsync<List<CharaDataMetaInfoDto>>(nameof(CharaDataGetShared)).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to get shared chara data");
return [];
}
}
public async Task<CharaDataDownloadDto?> CharaDataDownload(string id)
{
if (!IsConnected) return null;
try
{
Logger.LogDebug("Getting download chara data for {id}", id);
return await _mareHub!.InvokeAsync<CharaDataDownloadDto>(nameof(CharaDataDownload), id).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to get download chara data for {id}", id);
return null;
}
}
}