Add GPose Together (#82)
* add api glue * most of gpose together impl * more cleanup and impl * more impl * minor fixes and chara name abbreviations --------- Co-authored-by: Stanley Dimant <root.darkarchon@outlook.com>
This commit is contained in:
2
MareAPI
2
MareAPI
Submodule MareAPI updated: 4e4b2dab17...8b77956ec8
@@ -95,7 +95,7 @@ public sealed class IpcCallerBrio : IIpcCaller
|
|||||||
if (gameObject == null) return default;
|
if (gameObject == null) return default;
|
||||||
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
|
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||||
if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default;
|
if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default;
|
||||||
_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue);
|
//_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue);
|
||||||
|
|
||||||
return new WorldData()
|
return new WorldData()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public unsafe class VfxSpawnManager : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
mareMediator.Subscribe<GposeEndMessage>(this, (msg) =>
|
mareMediator.Subscribe<GposeEndMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
ChangeSpawnVisibility(0.5f);
|
RestoreSpawnVisiblity();
|
||||||
});
|
});
|
||||||
mareMediator.Subscribe<CutsceneStartMessage>(this, (msg) =>
|
mareMediator.Subscribe<CutsceneStartMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
@@ -47,19 +47,27 @@ public unsafe class VfxSpawnManager : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
mareMediator.Subscribe<CutsceneEndMessage>(this, (msg) =>
|
mareMediator.Subscribe<CutsceneEndMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
ChangeSpawnVisibility(0.5f);
|
RestoreSpawnVisiblity();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe void RestoreSpawnVisiblity()
|
||||||
|
{
|
||||||
|
foreach (var vfx in _spawnedObjects)
|
||||||
|
{
|
||||||
|
((VfxStruct*)vfx.Value.Address)->Alpha = vfx.Value.Visibility;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void ChangeSpawnVisibility(float visibility)
|
private unsafe void ChangeSpawnVisibility(float visibility)
|
||||||
{
|
{
|
||||||
foreach (var vfx in _spawnedObjects)
|
foreach (var vfx in _spawnedObjects)
|
||||||
{
|
{
|
||||||
((VfxStruct*)vfx.Value)->Alpha = visibility;
|
((VfxStruct*)vfx.Value.Address)->Alpha = visibility;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<Guid, nint> _spawnedObjects = [];
|
private readonly Dictionary<Guid, (nint Address, float Visibility)> _spawnedObjects = [];
|
||||||
|
|
||||||
private VfxStruct* SpawnStatic(string path, Vector3 pos, Quaternion rotation, float r, float g, float b, float a, Vector3 scale)
|
private VfxStruct* SpawnStatic(string path, Vector3 pos, Quaternion rotation, float r, float g, float b, float a, Vector3 scale)
|
||||||
{
|
{
|
||||||
@@ -106,17 +114,29 @@ public unsafe class VfxSpawnManager : DisposableMediatorSubscriberBase
|
|||||||
Guid guid = Guid.NewGuid();
|
Guid guid = Guid.NewGuid();
|
||||||
Logger.LogDebug("Spawned VFX at {pos}, {rot}: 0x{ptr:X}", position, rotation, (nint)vfx);
|
Logger.LogDebug("Spawned VFX at {pos}, {rot}: 0x{ptr:X}", position, rotation, (nint)vfx);
|
||||||
|
|
||||||
_spawnedObjects[guid] = (nint)vfx;
|
_spawnedObjects[guid] = ((nint)vfx, a);
|
||||||
|
|
||||||
return guid;
|
return guid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DespawnObject(Guid id)
|
public unsafe void MoveObject(Guid id, Vector3 newPosition)
|
||||||
{
|
{
|
||||||
if (_spawnedObjects.Remove<Guid, nint>(id, out var vfx))
|
if (_spawnedObjects.TryGetValue(id, out var vfxValue))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Despawning {obj:X}", vfx);
|
if (vfxValue.Address == nint.Zero) return;
|
||||||
_staticVfxRemove((VfxStruct*)vfx);
|
var vfx = (VfxStruct*)vfxValue.Address;
|
||||||
|
vfx->Position = newPosition with { Y = newPosition.Y + 1 };
|
||||||
|
vfx->Flags |= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DespawnObject(Guid? id)
|
||||||
|
{
|
||||||
|
if (id == null) return;
|
||||||
|
if (_spawnedObjects.Remove(id.Value, out var value))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Despawning {obj:X}", value.Address);
|
||||||
|
_staticVfxRemove((VfxStruct*)value.Address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +145,7 @@ public unsafe class VfxSpawnManager : DisposableMediatorSubscriberBase
|
|||||||
foreach (var obj in _spawnedObjects.Values)
|
foreach (var obj in _spawnedObjects.Values)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Despawning {obj:X}", obj);
|
Logger.LogDebug("Despawning {obj:X}", obj);
|
||||||
_staticVfxRemove((VfxStruct*)obj);
|
_staticVfxRemove((VfxStruct*)obj.Address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ using MareSynchronos.WebAPI.SignalR;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.Services.CharaData;
|
||||||
|
|
||||||
using MareSynchronos;
|
using MareSynchronos;
|
||||||
|
|
||||||
@@ -118,6 +119,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<CharaDataFileHandler>();
|
collection.AddSingleton<CharaDataFileHandler>();
|
||||||
collection.AddSingleton<CharaDataCharacterHandler>();
|
collection.AddSingleton<CharaDataCharacterHandler>();
|
||||||
collection.AddSingleton<CharaDataNearbyManager>();
|
collection.AddSingleton<CharaDataNearbyManager>();
|
||||||
|
collection.AddSingleton<CharaDataGposeTogetherManager>();
|
||||||
|
|
||||||
collection.AddSingleton<VfxSpawnManager>();
|
collection.AddSingleton<VfxSpawnManager>();
|
||||||
collection.AddSingleton<BlockedCharacterHandler>();
|
collection.AddSingleton<BlockedCharacterHandler>();
|
||||||
|
|||||||
@@ -0,0 +1,670 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
|
using MareSynchronos.Interop;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.Services.CharaData.Models;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData;
|
||||||
|
|
||||||
|
public class CharaDataGposeTogetherManager : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly IpcCallerBrio _brio;
|
||||||
|
private readonly SemaphoreSlim _charaDataCreationSemaphore = new(1, 1);
|
||||||
|
private readonly CharaDataFileHandler _charaDataFileHandler;
|
||||||
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly Dictionary<string, GposeLobbyUserData> _usersInLobby = [];
|
||||||
|
private readonly VfxSpawnManager _vfxSpawnManager;
|
||||||
|
private (CharacterData ApiData, CharaDataDownloadDto Dto)? _lastCreatedCharaData;
|
||||||
|
private PoseData? _lastDeltaPoseData;
|
||||||
|
private PoseData? _lastFullPoseData;
|
||||||
|
private WorldData? _lastWorldData;
|
||||||
|
private CancellationTokenSource _lobbyCts = new();
|
||||||
|
private int _poseGenerationExecutions = 0;
|
||||||
|
|
||||||
|
public CharaDataGposeTogetherManager(ILogger<CharaDataGposeTogetherManager> logger, MareMediator mediator,
|
||||||
|
ApiController apiController, IpcCallerBrio brio, DalamudUtilService dalamudUtil, VfxSpawnManager vfxSpawnManager,
|
||||||
|
CharaDataFileHandler charaDataFileHandler, CharaDataManager charaDataManager) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
Mediator.Subscribe<GposeLobbyUserJoin>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnUserJoinLobby(msg.UserData);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GPoseLobbyUserLeave>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnUserLeaveLobby(msg.UserData);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GPoseLobbyReceiveCharaData>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnReceiveCharaData(msg.CharaDataDownloadDto);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GPoseLobbyReceivePoseData>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnReceivePoseData(msg.UserData, msg.PoseData);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GPoseLobbyReceiveWorldData>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnReceiveWorldData(msg.UserData, msg.WorldData);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_usersInLobby.Count > 0 && !string.IsNullOrEmpty(CurrentGPoseLobbyId))
|
||||||
|
{
|
||||||
|
JoinGPoseLobby(CurrentGPoseLobbyId, isReconnecting: true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LeaveGPoseLobby();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GposeStartMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnEnterGpose();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GposeEndMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnExitGpose();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnFrameworkUpdate();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnCutsceneFrameworkUpdate();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
LeaveGPoseLobby();
|
||||||
|
});
|
||||||
|
|
||||||
|
_apiController = apiController;
|
||||||
|
_brio = brio;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_vfxSpawnManager = vfxSpawnManager;
|
||||||
|
_charaDataFileHandler = charaDataFileHandler;
|
||||||
|
_charaDataManager = charaDataManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? CurrentGPoseLobbyId { get; private set; }
|
||||||
|
public string? LastGPoseLobbyId { get; private set; }
|
||||||
|
|
||||||
|
public IEnumerable<GposeLobbyUserData> UsersInLobby => _usersInLobby.Values;
|
||||||
|
|
||||||
|
public (bool SameMap, bool SameServer, bool SameEverything) IsOnSameMapAndServer(GposeLobbyUserData data)
|
||||||
|
{
|
||||||
|
return (data.Map.RowId == _lastWorldData?.LocationInfo.MapId, data.WorldData?.LocationInfo.ServerId == _lastWorldData?.LocationInfo.ServerId, data.WorldData?.LocationInfo == _lastWorldData?.LocationInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PushCharacterDownloadDto()
|
||||||
|
{
|
||||||
|
var playerData = await _charaDataFileHandler.CreatePlayerData().ConfigureAwait(false);
|
||||||
|
if (playerData == null) return;
|
||||||
|
if (!string.Equals(playerData.DataHash.Value, _lastCreatedCharaData?.ApiData.DataHash.Value, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
List<GamePathEntry> filegamePaths = [.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player]
|
||||||
|
.Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))];
|
||||||
|
List<GamePathEntry> fileSwapPaths = [.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player]
|
||||||
|
.Where(u => !string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.FileSwapPath, path))];
|
||||||
|
await _charaDataManager.UploadFiles([.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player]
|
||||||
|
.Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))])
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
CharaDataDownloadDto charaDataDownloadDto = new($"GPOSELOBBY:{CurrentGPoseLobbyId}", new(_apiController.UID))
|
||||||
|
{
|
||||||
|
UpdatedDate = DateTime.UtcNow,
|
||||||
|
ManipulationData = playerData.ManipulationData,
|
||||||
|
CustomizeData = playerData.CustomizePlusData[API.Data.Enum.ObjectKind.Player],
|
||||||
|
FileGamePaths = filegamePaths,
|
||||||
|
FileSwaps = fileSwapPaths,
|
||||||
|
GlamourerData = playerData.GlamourerData[API.Data.Enum.ObjectKind.Player],
|
||||||
|
};
|
||||||
|
|
||||||
|
_lastCreatedCharaData = (playerData, charaDataDownloadDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastFullPoseData = null;
|
||||||
|
_lastWorldData = null;
|
||||||
|
|
||||||
|
if (_lastCreatedCharaData != null)
|
||||||
|
await _apiController.GposeLobbyPushCharacterData(_lastCreatedCharaData.Value.Dto)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void CreateNewLobby()
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
ClearLobby();
|
||||||
|
CurrentGPoseLobbyId = await _apiController.GposeLobbyCreate().ConfigureAwait(false);
|
||||||
|
if (!string.IsNullOrEmpty(CurrentGPoseLobbyId))
|
||||||
|
{
|
||||||
|
_ = GposeWorldPositionBackgroundTask(_lobbyCts.Token);
|
||||||
|
_ = GposePoseDataBackgroundTask(_lobbyCts.Token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void JoinGPoseLobby(string joinLobbyId, bool isReconnecting = false)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var otherUsers = await _apiController.GposeLobbyJoin(joinLobbyId).ConfigureAwait(false);
|
||||||
|
ClearLobby();
|
||||||
|
if (otherUsers.Any())
|
||||||
|
{
|
||||||
|
LastGPoseLobbyId = string.Empty;
|
||||||
|
|
||||||
|
foreach (var user in otherUsers)
|
||||||
|
{
|
||||||
|
OnUserJoinLobby(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentGPoseLobbyId = joinLobbyId;
|
||||||
|
_ = GposeWorldPositionBackgroundTask(_lobbyCts.Token);
|
||||||
|
_ = GposePoseDataBackgroundTask(_lobbyCts.Token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LeaveGPoseLobby();
|
||||||
|
LastGPoseLobbyId = string.Empty;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void LeaveGPoseLobby()
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var left = await _apiController.GposeLobbyLeave().ConfigureAwait(false);
|
||||||
|
if (left)
|
||||||
|
{
|
||||||
|
if (_usersInLobby.Count != 0)
|
||||||
|
{
|
||||||
|
LastGPoseLobbyId = CurrentGPoseLobbyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearLobby(revertCharas: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
ClearLobby(revertCharas: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearLobby(bool revertCharas = false)
|
||||||
|
{
|
||||||
|
_lobbyCts.Cancel();
|
||||||
|
_lobbyCts.Dispose();
|
||||||
|
_lobbyCts = new();
|
||||||
|
CurrentGPoseLobbyId = string.Empty;
|
||||||
|
foreach (var user in _usersInLobby.ToDictionary())
|
||||||
|
{
|
||||||
|
if (revertCharas)
|
||||||
|
_charaDataManager.RevertChara(user.Value.HandledChara);
|
||||||
|
OnUserLeaveLobby(user.Value.UserData);
|
||||||
|
}
|
||||||
|
_usersInLobby.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateJsonFromPoseData(PoseData? poseData)
|
||||||
|
{
|
||||||
|
if (poseData == null) return "{}";
|
||||||
|
|
||||||
|
var node = new JsonObject();
|
||||||
|
node["Bones"] = new JsonObject();
|
||||||
|
foreach (var bone in poseData.Value.Bones)
|
||||||
|
{
|
||||||
|
node["Bones"]![bone.Key] = new JsonObject();
|
||||||
|
node["Bones"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["Bones"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["Bones"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
}
|
||||||
|
node["MainHand"] = new JsonObject();
|
||||||
|
foreach (var bone in poseData.Value.MainHand)
|
||||||
|
{
|
||||||
|
node["MainHand"]![bone.Key] = new JsonObject();
|
||||||
|
node["MainHand"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["MainHand"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["MainHand"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
}
|
||||||
|
node["OffHand"] = new JsonObject();
|
||||||
|
foreach (var bone in poseData.Value.OffHand)
|
||||||
|
{
|
||||||
|
node["OffHand"]![bone.Key] = new JsonObject();
|
||||||
|
node["OffHand"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["OffHand"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["OffHand"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace(node.ToJsonString(new System.Text.Json.JsonSerializerOptions() { WriteIndented = true }));
|
||||||
|
|
||||||
|
return node.ToJsonString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PoseData CreatePoseDataFromJson(string json, PoseData? fullPoseData = null)
|
||||||
|
{
|
||||||
|
PoseData output = new();
|
||||||
|
output.Bones = new(StringComparer.Ordinal);
|
||||||
|
output.MainHand = new(StringComparer.Ordinal);
|
||||||
|
output.OffHand = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
float getRounded(string number)
|
||||||
|
{
|
||||||
|
return float.Round(float.Parse(number, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoneData createBoneData(JsonNode boneJson)
|
||||||
|
{
|
||||||
|
BoneData outputBoneData = new();
|
||||||
|
outputBoneData.Exists = true;
|
||||||
|
var posString = boneJson["Position"]!.ToString();
|
||||||
|
var pos = posString.Split(",", StringSplitOptions.TrimEntries);
|
||||||
|
outputBoneData.PositionX = getRounded(pos[0]);
|
||||||
|
outputBoneData.PositionY = getRounded(pos[1]);
|
||||||
|
outputBoneData.PositionZ = getRounded(pos[2]);
|
||||||
|
|
||||||
|
var scaString = boneJson["Scale"]!.ToString();
|
||||||
|
var sca = scaString.Split(",", StringSplitOptions.TrimEntries);
|
||||||
|
outputBoneData.ScaleX = getRounded(sca[0]);
|
||||||
|
outputBoneData.ScaleY = getRounded(sca[1]);
|
||||||
|
outputBoneData.ScaleZ = getRounded(sca[2]);
|
||||||
|
|
||||||
|
var rotString = boneJson["Rotation"]!.ToString();
|
||||||
|
var rot = rotString.Split(",", StringSplitOptions.TrimEntries);
|
||||||
|
outputBoneData.RotationX = getRounded(rot[0]);
|
||||||
|
outputBoneData.RotationY = getRounded(rot[1]);
|
||||||
|
outputBoneData.RotationZ = getRounded(rot[2]);
|
||||||
|
outputBoneData.RotationW = getRounded(rot[3]);
|
||||||
|
return outputBoneData;
|
||||||
|
}
|
||||||
|
|
||||||
|
var node = JsonNode.Parse(json)!;
|
||||||
|
var bones = node["Bones"]!.AsObject();
|
||||||
|
foreach (var bone in bones)
|
||||||
|
{
|
||||||
|
string name = bone.Key;
|
||||||
|
var boneJson = bone.Value!.AsObject();
|
||||||
|
BoneData outputBoneData = createBoneData(boneJson);
|
||||||
|
|
||||||
|
if (fullPoseData != null)
|
||||||
|
{
|
||||||
|
if (fullPoseData.Value.Bones.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData)
|
||||||
|
{
|
||||||
|
output.Bones[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.Bones[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var mainHand = node["MainHand"]!.AsObject();
|
||||||
|
foreach (var bone in mainHand)
|
||||||
|
{
|
||||||
|
string name = bone.Key;
|
||||||
|
var boneJson = bone.Value!.AsObject();
|
||||||
|
BoneData outputBoneData = createBoneData(boneJson);
|
||||||
|
|
||||||
|
if (fullPoseData != null)
|
||||||
|
{
|
||||||
|
if (fullPoseData.Value.MainHand.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData)
|
||||||
|
{
|
||||||
|
output.MainHand[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.MainHand[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var offhand = node["OffHand"]!.AsObject();
|
||||||
|
foreach (var bone in offhand)
|
||||||
|
{
|
||||||
|
string name = bone.Key;
|
||||||
|
var boneJson = bone.Value!.AsObject();
|
||||||
|
BoneData outputBoneData = createBoneData(boneJson);
|
||||||
|
|
||||||
|
if (fullPoseData != null)
|
||||||
|
{
|
||||||
|
if (fullPoseData.Value.OffHand.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData)
|
||||||
|
{
|
||||||
|
output.OffHand[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.OffHand[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPoseData != null)
|
||||||
|
output.IsDelta = true;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GposePoseDataBackgroundTask(CancellationToken ct)
|
||||||
|
{
|
||||||
|
_lastFullPoseData = null;
|
||||||
|
_lastDeltaPoseData = null;
|
||||||
|
_poseGenerationExecutions = 0;
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(10), ct).ConfigureAwait(false);
|
||||||
|
if (!_dalamudUtil.IsInGpose) continue;
|
||||||
|
if (_usersInLobby.Count == 0) continue;
|
||||||
|
|
||||||
|
var chara = await _dalamudUtil.GetPlayerCharacterAsync().ConfigureAwait(false);
|
||||||
|
if (_dalamudUtil.IsInGpose)
|
||||||
|
{
|
||||||
|
chara = (IPlayerCharacter?)(await _dalamudUtil.GetGposeCharacterFromObjectTableByNameAsync(chara.Name.TextValue, _dalamudUtil.IsInGpose).ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
if (chara == null || chara.Address == nint.Zero) continue;
|
||||||
|
|
||||||
|
var poseJson = await _brio.GetPoseAsync(chara.Address).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(poseJson)) continue;
|
||||||
|
|
||||||
|
var poseData = CreatePoseDataFromJson(poseJson, _poseGenerationExecutions++ >= 12 ? null : _lastFullPoseData);
|
||||||
|
if (!poseData.IsDelta)
|
||||||
|
{
|
||||||
|
_lastFullPoseData = poseData;
|
||||||
|
_lastDeltaPoseData = null;
|
||||||
|
_poseGenerationExecutions = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deltaIsSame = _lastDeltaPoseData != null &&
|
||||||
|
(poseData.Bones.Keys.All(k => _lastDeltaPoseData.Value.Bones.ContainsKey(k)
|
||||||
|
&& poseData.Bones.Values.All(k => _lastDeltaPoseData.Value.Bones.ContainsValue(k))));
|
||||||
|
|
||||||
|
if ((poseData.Bones.Any() || poseData.MainHand.Any() || poseData.OffHand.Any())
|
||||||
|
&& (!poseData.IsDelta || (poseData.IsDelta && !deltaIsSame)))
|
||||||
|
await _apiController.GposeLobbyPushPoseData(poseData).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (poseData.IsDelta)
|
||||||
|
_lastDeltaPoseData = poseData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GposeWorldPositionBackgroundTask(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(_dalamudUtil.IsInGpose ? 10 : 1), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// if there are no players in lobby, don't do anything
|
||||||
|
if (_usersInLobby.Count == 0) continue;
|
||||||
|
|
||||||
|
// get own player data
|
||||||
|
var player = (Dalamud.Game.ClientState.Objects.Types.ICharacter?)(await _dalamudUtil.GetPlayerCharacterAsync().ConfigureAwait(false));
|
||||||
|
if (player == null) continue;
|
||||||
|
WorldData worldData;
|
||||||
|
if (_dalamudUtil.IsInGpose)
|
||||||
|
{
|
||||||
|
player = await _dalamudUtil.GetGposeCharacterFromObjectTableByNameAsync(player.Name.TextValue, true).ConfigureAwait(false);
|
||||||
|
if (player == null) continue;
|
||||||
|
worldData = (await _brio.GetTransformAsync(player.Address).ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var rotQuaternion = Quaternion.CreateFromAxisAngle(new Vector3(0, 1, 0), player.Rotation);
|
||||||
|
worldData = new()
|
||||||
|
{
|
||||||
|
PositionX = player.Position.X,
|
||||||
|
PositionY = player.Position.Y,
|
||||||
|
PositionZ = player.Position.Z,
|
||||||
|
RotationW = rotQuaternion.W,
|
||||||
|
RotationX = rotQuaternion.X,
|
||||||
|
RotationY = rotQuaternion.Y,
|
||||||
|
RotationZ = rotQuaternion.Z,
|
||||||
|
ScaleX = 1,
|
||||||
|
ScaleY = 1,
|
||||||
|
ScaleZ = 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var loc = await _dalamudUtil.GetMapDataAsync().ConfigureAwait(false);
|
||||||
|
worldData.LocationInfo = loc;
|
||||||
|
|
||||||
|
if (worldData != _lastWorldData)
|
||||||
|
{
|
||||||
|
await _apiController.GposeLobbyPushWorldData(worldData).ConfigureAwait(false);
|
||||||
|
_lastWorldData = worldData;
|
||||||
|
Logger.LogTrace("WorldData (gpose: {gpose}): {data}", _dalamudUtil.IsInGpose, worldData);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in _usersInLobby)
|
||||||
|
{
|
||||||
|
if (!entry.Value.HasWorldDataUpdate || _dalamudUtil.IsInGpose) continue;
|
||||||
|
|
||||||
|
var entryWorldData = entry.Value.WorldData!.Value;
|
||||||
|
|
||||||
|
if (worldData.LocationInfo.MapId == entryWorldData.LocationInfo.MapId && worldData.LocationInfo.DivisionId == entryWorldData.LocationInfo.DivisionId
|
||||||
|
&& (worldData.LocationInfo.HouseId != entryWorldData.LocationInfo.HouseId
|
||||||
|
|| worldData.LocationInfo.WardId != entryWorldData.LocationInfo.WardId
|
||||||
|
|| entryWorldData.LocationInfo.ServerId != worldData.LocationInfo.ServerId))
|
||||||
|
{
|
||||||
|
if (entry.Value.SpawnedVfxId == null)
|
||||||
|
{
|
||||||
|
// spawn if it doesn't exist yet
|
||||||
|
entry.Value.LastWorldPosition = new Vector3(entryWorldData.PositionX, entryWorldData.PositionY, entryWorldData.PositionZ);
|
||||||
|
entry.Value.SpawnedVfxId = await _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.SpawnObject(entry.Value.LastWorldPosition.Value,
|
||||||
|
Quaternion.Identity, Vector3.One, 0.5f, 0.1f, 0.5f, 0.9f)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// move object via lerp if it does exist
|
||||||
|
var newPosition = new Vector3(entryWorldData.PositionX, entryWorldData.PositionY, entryWorldData.PositionZ);
|
||||||
|
if (newPosition != entry.Value.LastWorldPosition)
|
||||||
|
{
|
||||||
|
entry.Value.UpdateStart = DateTime.UtcNow;
|
||||||
|
entry.Value.TargetWorldPosition = newPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(entry.Value.SpawnedVfxId)).ConfigureAwait(false);
|
||||||
|
entry.Value.SpawnedVfxId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCutsceneFrameworkUpdate()
|
||||||
|
{
|
||||||
|
foreach (var kvp in _usersInLobby)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(kvp.Value.AssociatedCharaName))
|
||||||
|
{
|
||||||
|
kvp.Value.Address = _dalamudUtil.GetGposeCharacterFromObjectTableByName(kvp.Value.AssociatedCharaName, true)?.Address ?? nint.Zero;
|
||||||
|
if (kvp.Value.Address == nint.Zero)
|
||||||
|
{
|
||||||
|
kvp.Value.AssociatedCharaName = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kvp.Value.Address != nint.Zero && (kvp.Value.HasWorldDataUpdate || kvp.Value.HasPoseDataUpdate))
|
||||||
|
{
|
||||||
|
bool hadPoseDataUpdate = kvp.Value.HasPoseDataUpdate;
|
||||||
|
bool hadWorldDataUpdate = kvp.Value.HasWorldDataUpdate;
|
||||||
|
kvp.Value.HasPoseDataUpdate = false;
|
||||||
|
kvp.Value.HasWorldDataUpdate = false;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
if (hadPoseDataUpdate && kvp.Value.ApplicablePoseData != null)
|
||||||
|
{
|
||||||
|
await _brio.SetPoseAsync(kvp.Value.Address, CreateJsonFromPoseData(kvp.Value.ApplicablePoseData)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
if (hadWorldDataUpdate && kvp.Value.WorldData != null)
|
||||||
|
{
|
||||||
|
await _brio.ApplyTransformAsync(kvp.Value.Address, kvp.Value.WorldData.Value).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnterGpose()
|
||||||
|
{
|
||||||
|
ResetOwnData();
|
||||||
|
foreach (var data in _usersInLobby.Values)
|
||||||
|
{
|
||||||
|
_ = _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(data.SpawnedVfxId));
|
||||||
|
data.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExitGpose()
|
||||||
|
{
|
||||||
|
ResetOwnData();
|
||||||
|
foreach (var data in _usersInLobby.Values)
|
||||||
|
{
|
||||||
|
data.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetOwnData()
|
||||||
|
{
|
||||||
|
_lastFullPoseData = null;
|
||||||
|
_lastDeltaPoseData = null;
|
||||||
|
_poseGenerationExecutions = 0;
|
||||||
|
_lastCreatedCharaData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate()
|
||||||
|
{
|
||||||
|
var frameworkTime = DateTime.UtcNow;
|
||||||
|
foreach (var kvp in _usersInLobby)
|
||||||
|
{
|
||||||
|
if (kvp.Value.SpawnedVfxId != null && kvp.Value.UpdateStart != null)
|
||||||
|
{
|
||||||
|
var secondsElasped = frameworkTime.Subtract(kvp.Value.UpdateStart.Value).TotalSeconds;
|
||||||
|
if (secondsElasped >= 1)
|
||||||
|
{
|
||||||
|
kvp.Value.LastWorldPosition = kvp.Value.TargetWorldPosition;
|
||||||
|
kvp.Value.TargetWorldPosition = null;
|
||||||
|
kvp.Value.UpdateStart = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var lerp = Vector3.Lerp(kvp.Value.LastWorldPosition ?? Vector3.One, kvp.Value.TargetWorldPosition ?? Vector3.One, (float)secondsElasped);
|
||||||
|
_vfxSpawnManager.MoveObject(kvp.Value.SpawnedVfxId.Value, lerp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReceiveCharaData(CharaDataDownloadDto charaDataDownloadDto)
|
||||||
|
{
|
||||||
|
if (!_usersInLobby.TryGetValue(charaDataDownloadDto.Uploader.UID, out var lobbyData))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lobbyData.CharaData = charaDataDownloadDto;
|
||||||
|
if (lobbyData.Address != nint.Zero && !string.IsNullOrEmpty(lobbyData.AssociatedCharaName))
|
||||||
|
{
|
||||||
|
_ = ApplyCharaData(lobbyData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ApplyCharaData(GposeLobbyUserData userData)
|
||||||
|
{
|
||||||
|
if (userData.CharaData == null || userData.Address == nint.Zero || string.IsNullOrEmpty(userData.AssociatedCharaName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _charaDataCreationSemaphore.WaitAsync(_lobbyCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _charaDataManager.ApplyCharaData(userData.CharaData!, userData.AssociatedCharaName).ConfigureAwait(false);
|
||||||
|
userData.LastAppliedCharaDataDate = userData.CharaData.UpdatedDate;
|
||||||
|
userData.HasPoseDataUpdate = true;
|
||||||
|
userData.HasWorldDataUpdate = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_charaDataCreationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _charaDataSpawnSemaphore = new(1, 1);
|
||||||
|
|
||||||
|
internal async Task SpawnAndApplyData(GposeLobbyUserData userData)
|
||||||
|
{
|
||||||
|
if (userData.CharaData == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _charaDataSpawnSemaphore.WaitAsync(_lobbyCts.Token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userData.HasPoseDataUpdate = false;
|
||||||
|
userData.HasWorldDataUpdate = false;
|
||||||
|
var chara = await _charaDataManager.SpawnAndApplyData(userData.CharaData).ConfigureAwait(false);
|
||||||
|
if (chara == null) return;
|
||||||
|
userData.HandledChara = chara;
|
||||||
|
userData.AssociatedCharaName = chara.Name;
|
||||||
|
userData.HasPoseDataUpdate = true;
|
||||||
|
userData.HasWorldDataUpdate = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_charaDataSpawnSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReceivePoseData(UserData userData, PoseData poseData)
|
||||||
|
{
|
||||||
|
if (!_usersInLobby.TryGetValue(userData.UID, out var lobbyData))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poseData.IsDelta)
|
||||||
|
lobbyData.DeltaPoseData = poseData;
|
||||||
|
else
|
||||||
|
lobbyData.FullPoseData = poseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReceiveWorldData(UserData userData, WorldData worldData)
|
||||||
|
{
|
||||||
|
_usersInLobby[userData.UID].WorldData = worldData;
|
||||||
|
_ = _usersInLobby[userData.UID].SetWorldDataDescriptor(_dalamudUtil);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserJoinLobby(UserData userData)
|
||||||
|
{
|
||||||
|
if (_usersInLobby.ContainsKey(userData.UID))
|
||||||
|
OnUserLeaveLobby(userData);
|
||||||
|
_usersInLobby[userData.UID] = new(userData);
|
||||||
|
_lastFullPoseData = null;
|
||||||
|
_lastWorldData = null;
|
||||||
|
_ = PushCharacterDownloadDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserLeaveLobby(UserData msg)
|
||||||
|
{
|
||||||
|
_usersInLobby.Remove(msg.UID, out var existingData);
|
||||||
|
if (existingData != default)
|
||||||
|
{
|
||||||
|
_ = _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(existingData.SpawnedVfxId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ using MareSynchronos.Services.Mediator;
|
|||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
@@ -23,7 +24,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly CharaDataFileHandler _fileHandler;
|
private readonly CharaDataFileHandler _fileHandler;
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly Dictionary<string, CharaDataMetaInfoExtendedDto?> _metaInfoCache = [];
|
private readonly ConcurrentDictionary<string, CharaDataMetaInfoExtendedDto?> _metaInfoCache = [];
|
||||||
private readonly List<CharaDataMetaInfoExtendedDto> _nearbyData = [];
|
private readonly List<CharaDataMetaInfoExtendedDto> _nearbyData = [];
|
||||||
private readonly CharaDataNearbyManager _nearbyManager;
|
private readonly CharaDataNearbyManager _nearbyManager;
|
||||||
private readonly CharaDataCharacterHandler _characterHandler;
|
private readonly CharaDataCharacterHandler _characterHandler;
|
||||||
@@ -106,6 +107,23 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
public Task<(string Output, bool Success)>? UploadTask { get; set; }
|
public Task<(string Output, bool Success)>? UploadTask { get; set; }
|
||||||
public bool BrioAvailable => _ipcManager.Brio.APIAvailable;
|
public bool BrioAvailable => _ipcManager.Brio.APIAvailable;
|
||||||
|
|
||||||
|
public Task ApplyCharaData(CharaDataDownloadDto dataDownloadDto, string charaName)
|
||||||
|
{
|
||||||
|
return UiBlockingComputation = DataApplicationTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(charaName)) return;
|
||||||
|
|
||||||
|
CharaDataMetaInfoDto metaInfo = new(dataDownloadDto.Id, dataDownloadDto.Uploader)
|
||||||
|
{
|
||||||
|
CanBeDownloaded = true,
|
||||||
|
Description = $"Data from {dataDownloadDto.Uploader.AliasOrUID} for {dataDownloadDto.Id}",
|
||||||
|
UpdatedDate = dataDownloadDto.UpdatedDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
await DownloadAndAplyDataAsync(charaName, dataDownloadDto, metaInfo, false).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public Task ApplyCharaData(CharaDataMetaInfoDto dataMetaInfoDto, string charaName)
|
public Task ApplyCharaData(CharaDataMetaInfoDto dataMetaInfoDto, string charaName)
|
||||||
{
|
{
|
||||||
return UiBlockingComputation = DataApplicationTask = Task.Run(async () =>
|
return UiBlockingComputation = DataApplicationTask = Task.Run(async () =>
|
||||||
@@ -300,7 +318,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
if (ret)
|
if (ret)
|
||||||
{
|
{
|
||||||
_ownCharaData.Remove(dto.Id);
|
_ownCharaData.Remove(dto.Id);
|
||||||
_metaInfoCache.Remove(dto.FullId);
|
_metaInfoCache.Remove(dto.FullId, out _);
|
||||||
}
|
}
|
||||||
DistributeMetaInfo();
|
DistributeMetaInfo();
|
||||||
}
|
}
|
||||||
@@ -341,7 +359,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
foreach (var data in _ownCharaData)
|
foreach (var data in _ownCharaData)
|
||||||
{
|
{
|
||||||
_metaInfoCache.Remove(data.Key);
|
_metaInfoCache.Remove(data.Key, out _);
|
||||||
}
|
}
|
||||||
_ownCharaData.Clear();
|
_ownCharaData.Clear();
|
||||||
UiBlockingComputation = GetAllDataTask = Task.Run(async () =>
|
UiBlockingComputation = GetAllDataTask = Task.Run(async () =>
|
||||||
@@ -518,6 +536,22 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<HandledCharaDataEntry?> SpawnAndApplyData(CharaDataDownloadDto charaDataDownloadDto)
|
||||||
|
{
|
||||||
|
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(charaDataDownloadDto, newActor.Name.TextValue).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return _characterHandler.HandledCharaData.FirstOrDefault(f => string.Equals(f.Name, newActor.Name.TextValue, StringComparison.Ordinal));
|
||||||
|
});
|
||||||
|
UiBlockingComputation = task;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<HandledCharaDataEntry?> SpawnAndApplyData(CharaDataMetaInfoDto charaDataMetaInfoDto)
|
public Task<HandledCharaDataEntry?> SpawnAndApplyData(CharaDataMetaInfoDto charaDataMetaInfoDto)
|
||||||
{
|
{
|
||||||
var task = Task.Run(async () =>
|
var task = Task.Run(async () =>
|
||||||
@@ -560,10 +594,14 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
return extended;
|
return extended;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _distributionSemaphore = new(1, 1);
|
||||||
|
|
||||||
private void DistributeMetaInfo()
|
private void DistributeMetaInfo()
|
||||||
{
|
{
|
||||||
_nearbyManager.UpdateSharedData(_metaInfoCache);
|
_distributionSemaphore.Wait();
|
||||||
_characterHandler.UpdateHandledData(_metaInfoCache);
|
_nearbyManager.UpdateSharedData(_metaInfoCache.ToDictionary());
|
||||||
|
_characterHandler.UpdateHandledData(_metaInfoCache.ToDictionary());
|
||||||
|
_distributionSemaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CacheData(CharaDataMetaInfoExtendedDto charaData)
|
private void CacheData(CharaDataMetaInfoExtendedDto charaData)
|
||||||
@@ -841,7 +879,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
if (!_dalamudUtilService.IsInGpose)
|
if (!_dalamudUtilService.IsInGpose)
|
||||||
Mediator.Publish(new HaltCharaDataCreation(Resume: true));
|
Mediator.Publish(new HaltCharaDataCreation(Resume: true));
|
||||||
|
|
||||||
if (_configService.Current.FavoriteCodes.TryGetValue(metaInfo.Uploader.UID + ":" + metaInfo.Id, out var favorite) && favorite != null)
|
if (metaInfo != null && _configService.Current.FavoriteCodes.TryGetValue(metaInfo.Uploader.UID + ":" + metaInfo.Id, out var favorite) && favorite != null)
|
||||||
{
|
{
|
||||||
favorite.LastDownloaded = DateTime.UtcNow;
|
favorite.LastDownloaded = DateTime.UtcNow;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
@@ -932,7 +970,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
charaDataDownloadDto.CustomizeData, token).ConfigureAwait(false);
|
charaDataDownloadDto.CustomizeData, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(string Result, bool Success)> UploadFiles(List<GamePathEntry> missingFileList, Func<Task>? postUpload = null)
|
public async Task<(string Result, bool Success)> UploadFiles(List<GamePathEntry> missingFileList, Func<Task>? postUpload = null)
|
||||||
{
|
{
|
||||||
UploadProgress = new ValueProgress<string>();
|
UploadProgress = new ValueProgress<string>();
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData
|
||||||
|
{
|
||||||
|
internal class CharaDataTogetherManager
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
174
MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs
Normal file
174
MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
using Dalamud.Utility;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData.Models;
|
||||||
|
|
||||||
|
public sealed record GposeLobbyUserData(UserData UserData)
|
||||||
|
{
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
HasWorldDataUpdate = WorldData != null;
|
||||||
|
HasPoseDataUpdate = ApplicablePoseData != null;
|
||||||
|
SpawnedVfxId = null;
|
||||||
|
LastAppliedCharaDataDate = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldData? _worldData;
|
||||||
|
public WorldData? WorldData
|
||||||
|
{
|
||||||
|
get => _worldData; set
|
||||||
|
{
|
||||||
|
_worldData = value;
|
||||||
|
HasWorldDataUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasWorldDataUpdate { get; set; } = false;
|
||||||
|
|
||||||
|
private PoseData? _fullPoseData;
|
||||||
|
private PoseData? _deltaPoseData;
|
||||||
|
|
||||||
|
public PoseData? FullPoseData
|
||||||
|
{
|
||||||
|
get => _fullPoseData;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fullPoseData = value;
|
||||||
|
ApplicablePoseData = CombinePoseData();
|
||||||
|
HasPoseDataUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PoseData? DeltaPoseData
|
||||||
|
{
|
||||||
|
get => _deltaPoseData;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_deltaPoseData = value;
|
||||||
|
ApplicablePoseData = CombinePoseData();
|
||||||
|
HasPoseDataUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PoseData? ApplicablePoseData { get; private set; }
|
||||||
|
public bool HasPoseDataUpdate { get; set; } = false;
|
||||||
|
public Guid? SpawnedVfxId { get; set; }
|
||||||
|
public Vector3? LastWorldPosition { get; set; }
|
||||||
|
public Vector3? TargetWorldPosition { get; set; }
|
||||||
|
public DateTime? UpdateStart { get; set; }
|
||||||
|
private CharaDataDownloadDto? _charaData;
|
||||||
|
public CharaDataDownloadDto? CharaData
|
||||||
|
{
|
||||||
|
get => _charaData; set
|
||||||
|
{
|
||||||
|
_charaData = value;
|
||||||
|
LastUpdatedCharaData = _charaData?.UpdatedDate ?? DateTime.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime LastUpdatedCharaData { get; private set; } = DateTime.MaxValue;
|
||||||
|
public DateTime LastAppliedCharaDataDate { get; set; } = DateTime.MinValue;
|
||||||
|
public nint Address { get; set; }
|
||||||
|
public string AssociatedCharaName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private PoseData? CombinePoseData()
|
||||||
|
{
|
||||||
|
if (DeltaPoseData == null && FullPoseData != null) return FullPoseData;
|
||||||
|
if (FullPoseData == null) return null;
|
||||||
|
|
||||||
|
PoseData output = FullPoseData!.Value.DeepClone();
|
||||||
|
PoseData delta = DeltaPoseData!.Value;
|
||||||
|
|
||||||
|
foreach (var bone in FullPoseData!.Value.Bones)
|
||||||
|
{
|
||||||
|
if (!delta.Bones.TryGetValue(bone.Key, out var data)) continue;
|
||||||
|
if (!data.Exists)
|
||||||
|
{
|
||||||
|
output.Bones.Remove(bone.Key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.Bones[bone.Key] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var bone in FullPoseData!.Value.MainHand)
|
||||||
|
{
|
||||||
|
if (!delta.MainHand.TryGetValue(bone.Key, out var data)) continue;
|
||||||
|
if (!data.Exists)
|
||||||
|
{
|
||||||
|
output.MainHand.Remove(bone.Key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.MainHand[bone.Key] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var bone in FullPoseData!.Value.OffHand)
|
||||||
|
{
|
||||||
|
if (!delta.OffHand.TryGetValue(bone.Key, out var data)) continue;
|
||||||
|
if (!data.Exists)
|
||||||
|
{
|
||||||
|
output.OffHand.Remove(bone.Key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.OffHand[bone.Key] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string WorldDataDescriptor { get; private set; } = string.Empty;
|
||||||
|
public Vector2 MapCoordinates { get; private set; }
|
||||||
|
public Lumina.Excel.Sheets.Map Map { get; private set; }
|
||||||
|
public HandledCharaDataEntry? HandledChara { get; set; }
|
||||||
|
|
||||||
|
public async Task SetWorldDataDescriptor(DalamudUtilService dalamudUtilService)
|
||||||
|
{
|
||||||
|
if (WorldData == null)
|
||||||
|
{
|
||||||
|
WorldDataDescriptor = "No World Data found";
|
||||||
|
}
|
||||||
|
|
||||||
|
var worldData = WorldData!.Value;
|
||||||
|
MapCoordinates = await dalamudUtilService.RunOnFrameworkThread(() =>
|
||||||
|
MapUtil.WorldToMap(new Vector2(worldData.PositionX, worldData.PositionY), dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
Map = dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map;
|
||||||
|
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.AppendLine("Server: " + dalamudUtilService.WorldData.Value[(ushort)worldData.LocationInfo.ServerId]);
|
||||||
|
sb.AppendLine("Territory: " + dalamudUtilService.TerritoryData.Value[worldData.LocationInfo.TerritoryId]);
|
||||||
|
sb.AppendLine("Map: " + dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].MapName);
|
||||||
|
|
||||||
|
if (worldData.LocationInfo.WardId != 0)
|
||||||
|
sb.AppendLine("Ward #: " + worldData.LocationInfo.WardId);
|
||||||
|
if (worldData.LocationInfo.DivisionId != 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("Subdivision: " + worldData.LocationInfo.DivisionId switch
|
||||||
|
{
|
||||||
|
1 => "No",
|
||||||
|
2 => "Yes",
|
||||||
|
_ => "-"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (worldData.LocationInfo.HouseId != 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("House #: " + (worldData.LocationInfo.HouseId == 100 ? "Apartments" : worldData.LocationInfo.HouseId.ToString()));
|
||||||
|
}
|
||||||
|
if (worldData.LocationInfo.RoomId != 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("Apartment #: " + worldData.LocationInfo.RoomId);
|
||||||
|
}
|
||||||
|
sb.AppendLine("Coordinates: X: " + MapCoordinates.X.ToString("0.0", CultureInfo.InvariantCulture) + ", Y: " + MapCoordinates.Y.ToString("0.0", CultureInfo.InvariantCulture));
|
||||||
|
WorldDataDescriptor = sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using MareSynchronos.API.Data;
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.API.Dto;
|
using MareSynchronos.API.Dto;
|
||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.API.Dto.Group;
|
||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.PlayerData.Handlers;
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
@@ -101,6 +102,11 @@ public record GameObjectHandlerCreatedMessage(GameObjectHandler GameObjectHandle
|
|||||||
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 HaltCharaDataCreation(bool Resume = false) : SameThreadMessage;
|
||||||
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
||||||
|
public record GposeLobbyUserJoin(UserData UserData) : MessageBase;
|
||||||
|
public record GPoseLobbyUserLeave(UserData UserData) : MessageBase;
|
||||||
|
public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadDto) : MessageBase;
|
||||||
|
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
||||||
|
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
227
MareSynchronos/UI/CharaDataHubUi.GposeTogether.cs
Normal file
227
MareSynchronos/UI/CharaDataHubUi.GposeTogether.cs
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using ImGuiNET;
|
||||||
|
using MareSynchronos.Services.CharaData.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
|
internal sealed partial class CharaDataHubUi
|
||||||
|
{
|
||||||
|
private string _joinLobbyId = string.Empty;
|
||||||
|
private void DrawGposeTogether()
|
||||||
|
{
|
||||||
|
if (!_charaDataManager.BrioAvailable)
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", ImGuiColors.DalamudRed);
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_uiSharedService.ApiController.IsConnected)
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", ImGuiColors.DalamudRed);
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiSharedService.BigText("GPose Together");
|
||||||
|
DrawHelpFoldout("GPose together is a way to do multiplayer GPose sessions and collaborations." + UiSharedService.DoubleNewLine
|
||||||
|
+ "GPose together requires Brio to function. Only Brio is also supported for the actual posing interactions. Attempting to pose using other tools will lead to conflicts and exploding characters." + UiSharedService.DoubleNewLine
|
||||||
|
+ "To use GPose together you either create or join a GPose Together Lobby. After you and other people have joined, make sure that everyone is on the same map. "
|
||||||
|
+ "It is not required for you to be on the same server, DC or instance. Users that are on the same map will be drawn as moving purple wisps in the overworld, so you can easily find each other." + UiSharedService.DoubleNewLine
|
||||||
|
+ "Once you are close to each other you can initiate GPose. You must either assign or spawn characters for each of the lobby users. Their own poses and positions to their character will be automatically applied." + Environment.NewLine
|
||||||
|
+ "Pose and location data during GPose are updated approximately every 10-20s.");
|
||||||
|
|
||||||
|
using var disabled = ImRaii.Disabled(!_charaDataManager.BrioAvailable || !_uiSharedService.ApiController.IsConnected);
|
||||||
|
|
||||||
|
UiSharedService.DistanceSeparator();
|
||||||
|
_uiSharedService.BigText("Lobby Controls");
|
||||||
|
if (string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create New GPose Together Lobby"))
|
||||||
|
{
|
||||||
|
_charaDataGposeTogetherManager.CreateNewLobby();
|
||||||
|
}
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
ImGui.SetNextItemWidth(250);
|
||||||
|
ImGui.InputTextWithHint("##lobbyId", "GPose Lobby Id", ref _joinLobbyId, 30);
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Join GPose Together Lobby"))
|
||||||
|
{
|
||||||
|
_charaDataGposeTogetherManager.JoinGPoseLobby(_joinLobbyId);
|
||||||
|
_joinLobbyId = string.Empty;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(_charaDataGposeTogetherManager.LastGPoseLobbyId)
|
||||||
|
&& _uiSharedService.IconTextButton(FontAwesomeIcon.LongArrowAltRight, $"Rejoin Last Lobby {_charaDataGposeTogetherManager.LastGPoseLobbyId}"))
|
||||||
|
{
|
||||||
|
_charaDataGposeTogetherManager.JoinGPoseLobby(_charaDataGposeTogetherManager.LastGPoseLobbyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted("GPose Lobby");
|
||||||
|
ImGui.SameLine();
|
||||||
|
UiSharedService.ColorTextWrapped(_charaDataGposeTogetherManager.CurrentGPoseLobbyId, ImGuiColors.ParsedGreen);
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Clipboard))
|
||||||
|
{
|
||||||
|
ImGui.SetClipboardText(_charaDataGposeTogetherManager.CurrentGPoseLobbyId);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Copy Lobby ID to clipboard.");
|
||||||
|
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowLeft, "Leave GPose Lobby"))
|
||||||
|
{
|
||||||
|
_charaDataGposeTogetherManager.LeaveGPoseLobby();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Leave the current GPose lobby." + UiSharedService.TooltipSeparator + "Hold CTRL and click to leave.");
|
||||||
|
}
|
||||||
|
UiSharedService.DistanceSeparator();
|
||||||
|
using (ImRaii.Disabled(string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId)))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowUp, "Send Updated Character Data"))
|
||||||
|
{
|
||||||
|
_ = _charaDataGposeTogetherManager.PushCharacterDownloadDto();
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("This will send your current appearance, pose and world data to all users in the lobby.");
|
||||||
|
if (!_uiSharedService.IsInGpose)
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", ImGuiColors.DalamudYellow, 300);
|
||||||
|
}
|
||||||
|
UiSharedService.DistanceSeparator();
|
||||||
|
ImGui.TextUnformatted("Users In Lobby");
|
||||||
|
var gposeCharas = _dalamudUtilService.GetGposeCharactersFromObjectTable();
|
||||||
|
var self = _dalamudUtilService.GetPlayerCharacter();
|
||||||
|
gposeCharas = gposeCharas.Where(c => c != null && !string.Equals(c.Name.TextValue, self.Name.TextValue, StringComparison.Ordinal)).ToList();
|
||||||
|
|
||||||
|
using (ImRaii.Child("charaChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize))
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(3);
|
||||||
|
|
||||||
|
if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
|
||||||
|
{
|
||||||
|
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", ImGuiColors.DalamudYellow);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var user in _charaDataGposeTogetherManager.UsersInLobby)
|
||||||
|
{
|
||||||
|
DrawLobbyUser(user, gposeCharas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawLobbyUser(GposeLobbyUserData user,
|
||||||
|
IEnumerable<Dalamud.Game.ClientState.Objects.Types.ICharacter?> gposeCharas)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId(user.UserData.UID);
|
||||||
|
using var indent = ImRaii.PushIndent(5f);
|
||||||
|
var sameMapAndServer = _charaDataGposeTogetherManager.IsOnSameMapAndServer(user);
|
||||||
|
var width = ImGui.GetContentRegionAvail().X - 5;
|
||||||
|
UiSharedService.DrawGrouped(() =>
|
||||||
|
{
|
||||||
|
var availWidth = ImGui.GetContentRegionAvail().X;
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
var note = _serverConfigurationManager.GetNoteForUid(user.UserData.UID);
|
||||||
|
var userText = note == null ? user.UserData.AliasOrUID : $"{note} ({user.UserData.AliasOrUID})";
|
||||||
|
UiSharedService.ColorText(userText, ImGuiColors.ParsedGreen);
|
||||||
|
|
||||||
|
var buttonsize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowRight).X;
|
||||||
|
var buttonsize2 = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X;
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosX(availWidth - (buttonsize + buttonsize2 + ImGui.GetStyle().ItemSpacing.X));
|
||||||
|
using (ImRaii.Disabled(!_uiSharedService.IsInGpose || user.CharaData == null || user.Address == nint.Zero))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.ArrowRight))
|
||||||
|
{
|
||||||
|
_ = _charaDataGposeTogetherManager.ApplyCharaData(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Apply newly received character data to selected actor." + UiSharedService.TooltipSeparator + "Note: If the button is grayed out, the latest data has already been applied.");
|
||||||
|
ImGui.SameLine();
|
||||||
|
using (ImRaii.Disabled(!_uiSharedService.IsInGpose || user.CharaData == null || sameMapAndServer.SameEverything))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
|
||||||
|
{
|
||||||
|
_ = _charaDataGposeTogetherManager.SpawnAndApplyData(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Spawn new actor, apply character data and and assign it to this user." + UiSharedService.TooltipSeparator + "Note: If the button is grayed out, " +
|
||||||
|
"the user has not sent any character data or you are on the same map, server and instance. If the latter is the case, join a group with that user and assign the character to them.");
|
||||||
|
|
||||||
|
|
||||||
|
using (ImRaii.Group())
|
||||||
|
{
|
||||||
|
UiSharedService.ColorText("Map Info", ImGuiColors.DalamudGrey);
|
||||||
|
ImGui.SameLine();
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.ExternalLinkSquareAlt, ImGuiColors.DalamudGrey);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null)
|
||||||
|
{
|
||||||
|
_dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same map." : "You are not on the same map.") + UiSharedService.TooltipSeparator
|
||||||
|
+ "Note: Click to open the users location on your map." + Environment.NewLine
|
||||||
|
+ "Note: For GPose synchronization to work properly, you must be on the same map.");
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
||||||
|
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator
|
||||||
|
+ "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users.");
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
||||||
|
UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator +
|
||||||
|
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
|
||||||
|
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");
|
||||||
|
|
||||||
|
using (ImRaii.Disabled(!_uiSharedService.IsInGpose))
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(200);
|
||||||
|
using (var combo = ImRaii.Combo("##character", string.IsNullOrEmpty(user.AssociatedCharaName) ? "No character assigned" : CharaName(user.AssociatedCharaName)))
|
||||||
|
{
|
||||||
|
if (combo)
|
||||||
|
{
|
||||||
|
foreach (var chara in gposeCharas)
|
||||||
|
{
|
||||||
|
if (chara == null) continue;
|
||||||
|
|
||||||
|
if (ImGui.Selectable(CharaName(chara.Name.TextValue), chara.Address == user.Address))
|
||||||
|
{
|
||||||
|
user.AssociatedCharaName = chara.Name.TextValue;
|
||||||
|
user.Address = chara.Address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
using (ImRaii.Disabled(user.Address == nint.Zero))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Trash))
|
||||||
|
{
|
||||||
|
user.AssociatedCharaName = string.Empty;
|
||||||
|
user.Address = nint.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Unassign Actor for this user");
|
||||||
|
if (_uiSharedService.IsInGpose && user.Address == nint.Zero)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudRed);
|
||||||
|
UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5, width);
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ using MareSynchronos.MareConfiguration;
|
|||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.CharaData;
|
||||||
using MareSynchronos.Services.CharaData.Models;
|
using MareSynchronos.Services.CharaData.Models;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
@@ -26,6 +27,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly FileDialogManager _fileDialogManager;
|
private readonly FileDialogManager _fileDialogManager;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly CharaDataGposeTogetherManager _charaDataGposeTogetherManager;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private CancellationTokenSource _closalCts = new();
|
private CancellationTokenSource _closalCts = new();
|
||||||
@@ -75,7 +77,8 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
public CharaDataHubUi(ILogger<CharaDataHubUi> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
|
public CharaDataHubUi(ILogger<CharaDataHubUi> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
|
||||||
CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService,
|
CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService,
|
||||||
UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager,
|
UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager,
|
||||||
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager)
|
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager,
|
||||||
|
CharaDataGposeTogetherManager charaDataGposeTogetherManager)
|
||||||
: base(logger, mediator, "Loporrit Character Data Hub###LoporritCharaDataUI", performanceCollectorService)
|
: base(logger, mediator, "Loporrit Character Data Hub###LoporritCharaDataUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
SetWindowSizeConstraints();
|
SetWindowSizeConstraints();
|
||||||
@@ -88,6 +91,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_fileDialogManager = fileDialogManager;
|
_fileDialogManager = fileDialogManager;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_charaDataGposeTogetherManager = charaDataGposeTogetherManager;
|
||||||
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen |= _configService.Current.OpenMareHubOnGposeStart);
|
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen |= _configService.Current.OpenMareHubOnGposeStart);
|
||||||
Mediator.Subscribe<OpenCharaDataHubWithFilterMessage>(this, (msg) =>
|
Mediator.Subscribe<OpenCharaDataHubWithFilterMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
@@ -202,6 +206,16 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
_isHandlingSelf = _charaDataManager.HandledCharaData.Any(c => c.IsSelf);
|
_isHandlingSelf = _charaDataManager.HandledCharaData.Any(c => c.IsSelf);
|
||||||
if (_isHandlingSelf) _openMcdOnlineOnNextRun = false;
|
if (_isHandlingSelf) _openMcdOnlineOnNextRun = false;
|
||||||
|
|
||||||
|
using (var gposeTogetherTabItem = ImRaii.TabItem("GPose Together"))
|
||||||
|
{
|
||||||
|
if (gposeTogetherTabItem)
|
||||||
|
{
|
||||||
|
smallUi = true;
|
||||||
|
|
||||||
|
DrawGposeTogether();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
using (var applicationTabItem = ImRaii.TabItem("Data Application", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
using (var applicationTabItem = ImRaii.TabItem("Data Application", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
||||||
{
|
{
|
||||||
if (applicationTabItem)
|
if (applicationTabItem)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace MareSynchronos.UI;
|
|||||||
public partial class UiSharedService : DisposableMediatorSubscriberBase
|
public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
public const string TooltipSeparator = "--SEP--";
|
public const string TooltipSeparator = "--SEP--";
|
||||||
|
public static string DoubleNewLine => Environment.NewLine + Environment.NewLine;
|
||||||
|
|
||||||
public static readonly ImGuiWindowFlags PopupWindowFlags = ImGuiWindowFlags.NoResize |
|
public static readonly ImGuiWindowFlags PopupWindowFlags = ImGuiWindowFlags.NoResize |
|
||||||
ImGuiWindowFlags.NoScrollbar |
|
ImGuiWindowFlags.NoScrollbar |
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
using MareSynchronos.API.Data.Enum;
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
using MareSynchronos.API.Dto;
|
using MareSynchronos.API.Dto;
|
||||||
using MareSynchronos.API.Dto.Chat;
|
using MareSynchronos.API.Dto.Chat;
|
||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.API.Dto.Group;
|
||||||
using MareSynchronos.API.Dto.User;
|
using MareSynchronos.API.Dto.User;
|
||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static FFXIVClientStructs.FFXIV.Client.Game.UI.MapMarkerData.Delegates;
|
||||||
|
|
||||||
namespace MareSynchronos.WebAPI;
|
namespace MareSynchronos.WebAPI;
|
||||||
|
|
||||||
@@ -191,6 +194,41 @@ public partial class ApiController
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task Client_GposeLobbyJoin(UserData userData)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Client_GposeLobbyJoin: {dto}", userData);
|
||||||
|
ExecuteSafely(() => Mediator.Publish(new GposeLobbyUserJoin(userData)));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Client_GposeLobbyLeave(UserData userData)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Client_GposeLobbyLeave: {dto}", userData);
|
||||||
|
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyUserLeave(userData)));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Client_GposeLobbyPushCharacterData: {dto}", charaDownloadDto.Uploader);
|
||||||
|
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveCharaData(charaDownloadDto)));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Client_GposeLobbyPushPoseData: {dto}", userData);
|
||||||
|
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceivePoseData(userData, poseData)));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData)
|
||||||
|
{
|
||||||
|
//Logger.LogDebug("Client_GposeLobbyPushWorldData: {dto}", userData);
|
||||||
|
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData)));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public void OnDownloadReady(Action<Guid> act)
|
public void OnDownloadReady(Action<Guid> act)
|
||||||
{
|
{
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
@@ -323,6 +361,36 @@ public partial class ApiController
|
|||||||
_mareHub!.On(nameof(Client_UserUpdateSelfPairPermissions), act);
|
_mareHub!.On(nameof(Client_UserUpdateSelfPairPermissions), act);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnGposeLobbyJoin(Action<UserData> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_mareHub!.On(nameof(Client_GposeLobbyJoin), act);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGposeLobbyLeave(Action<UserData> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_mareHub!.On(nameof(Client_GposeLobbyLeave), act);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGposeLobbyPushCharacterData(Action<CharaDataDownloadDto> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_mareHub!.On(nameof(Client_GposeLobbyPushCharacterData), act);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGposeLobbyPushPoseData(Action<UserData, PoseData> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_mareHub!.On(nameof(Client_GposeLobbyPushPoseData), act);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGposeLobbyPushWorldData(Action<UserData, WorldData> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_mareHub!.On(nameof(Client_GposeLobbyPushWorldData), act);
|
||||||
|
}
|
||||||
|
|
||||||
private void ExecuteSafely(Action act)
|
private void ExecuteSafely(Action act)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using MareSynchronos.API.Data;
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.API.Dto.CharaData;
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
using MareSynchronos.Services.CharaData.Models;
|
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -134,4 +133,96 @@ public partial class ApiController
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> GposeLobbyCreate()
|
||||||
|
{
|
||||||
|
if (!IsConnected) return string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Creating GPose Lobby");
|
||||||
|
return await _mareHub!.InvokeAsync<string>(nameof(GposeLobbyCreate)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to create GPose lobby");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> GposeLobbyLeave()
|
||||||
|
{
|
||||||
|
if (!IsConnected) return true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Leaving current GPose Lobby");
|
||||||
|
return await _mareHub!.InvokeAsync<bool>(nameof(GposeLobbyLeave)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to leave GPose lobby");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UserData>> GposeLobbyJoin(string lobbyId)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return [];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Joining GPose Lobby {id}", lobbyId);
|
||||||
|
return await _mareHub!.InvokeAsync<List<UserData>>(nameof(GposeLobbyJoin), lobbyId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to join GPose lobby {id}", lobbyId);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Sending Chara Data to GPose Lobby");
|
||||||
|
await _mareHub!.InvokeAsync(nameof(GposeLobbyPushCharacterData), charaDownloadDto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to send Chara Data to GPose lobby");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task GposeLobbyPushPoseData(PoseData poseData)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Sending Pose Data to GPose Lobby");
|
||||||
|
await _mareHub!.InvokeAsync(nameof(GposeLobbyPushPoseData), poseData).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to send Pose Data to GPose lobby");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task GposeLobbyPushWorldData(WorldData worldData)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _mareHub!.InvokeAsync(nameof(GposeLobbyPushWorldData), worldData).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to send World Data to GPose lobby");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,6 +364,12 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
|||||||
OnUserChatMsg((dto) => _ = Client_UserChatMsg(dto));
|
OnUserChatMsg((dto) => _ = Client_UserChatMsg(dto));
|
||||||
OnGroupChatMsg((dto) => _ = Client_GroupChatMsg(dto));
|
OnGroupChatMsg((dto) => _ = Client_GroupChatMsg(dto));
|
||||||
|
|
||||||
|
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
|
||||||
|
OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto));
|
||||||
|
OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto));
|
||||||
|
OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data));
|
||||||
|
OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data));
|
||||||
|
|
||||||
_healthCheckTokenSource?.Cancel();
|
_healthCheckTokenSource?.Cancel();
|
||||||
_healthCheckTokenSource?.Dispose();
|
_healthCheckTokenSource?.Dispose();
|
||||||
_healthCheckTokenSource = new CancellationTokenSource();
|
_healthCheckTokenSource = new CancellationTokenSource();
|
||||||
|
|||||||
Reference in New Issue
Block a user