Files
LightlessClient/LightlessSync/WebAPI/SignalR/ApiController.cs
Tsubasa 5b3d00b90a API14 Updates - Migrate to IPlayerState (#113)
- use IPlayerState for DalamudUtilService and make things less async
- make LocationInfo work with ContentFinderData

Co-authored-by: Tsubasahane <wozaiha@gmail.com>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #113
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-committed-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
2025-12-28 03:26:07 +00:00

779 lines
33 KiB
C#

using System.Reflection;
using Dalamud.Utility;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto;
using LightlessSync.API.Dto.Chat;
using LightlessSync.API.Dto.User;
using LightlessSync.API.SignalR;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.WebAPI.SignalR;
using LightlessSync.WebAPI.SignalR.Utils;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
namespace LightlessSync.WebAPI;
#pragma warning disable MA0040
public sealed partial class ApiController : DisposableMediatorSubscriberBase, ILightlessHubClient
{
public const string MainServer = "Follow the light (Official Central Server)";
public const string MainServiceUri = "wss://sync.lightless-sync.org";
private readonly DalamudUtilService _dalamudUtil;
private readonly HubFactory _hubFactory;
private readonly PairCoordinator _pairCoordinator;
private readonly PairRequestService _pairRequestService;
private readonly ServerConfigurationManager _serverManager;
private readonly TokenProvider _tokenProvider;
private readonly LightlessConfigService _lightlessConfigService;
private CancellationTokenSource _connectionCancellationTokenSource;
private ConnectionDto? _connectionDto;
private bool _doNotNotifyOnNextInfo = false;
private CancellationTokenSource? _healthCheckTokenSource = new();
private bool _initialized;
private string? _lastUsedToken;
private HubConnection? _lightlessHub;
private ServerState _serverState;
private CensusUpdateMessage? _lastCensus;
private IReadOnlyList<ZoneChatChannelInfoDto> _zoneChatChannels = Array.Empty<ZoneChatChannelInfoDto>();
private IReadOnlyList<GroupChatChannelInfoDto> _groupChatChannels = Array.Empty<GroupChatChannelInfoDto>();
private event Action<ChatMessageDto>? ChatMessageReceived;
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
PairCoordinator pairCoordinator, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator,
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator)
{
_hubFactory = hubFactory;
_dalamudUtil = dalamudUtil;
_pairCoordinator = pairCoordinator;
_pairRequestService = pairRequestService;
_serverManager = serverManager;
_tokenProvider = tokenProvider;
_lightlessConfigService = lightlessConfigService;
_connectionCancellationTokenSource = new CancellationTokenSource();
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
Mediator.Subscribe<HubClosedMessage>(this, (msg) => LightlessHubOnClosed(msg.Exception));
Mediator.Subscribe<HubReconnectedMessage>(this, (msg) => _ = LightlessHubOnReconnectedAsync());
Mediator.Subscribe<HubReconnectingMessage>(this, (msg) => LightlessHubOnReconnecting(msg.Exception));
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => _ = CyclePauseAsync(msg.Pair));
Mediator.Subscribe<CensusUpdateMessage>(this, (msg) => _lastCensus = msg);
Mediator.Subscribe<PauseMessage>(this, (msg) => _ = PauseAsync(msg.UserData));
ServerState = ServerState.Offline;
if (_dalamudUtil.IsLoggedIn)
{
DalamudUtilOnLogIn();
}
}
public string AuthFailureMessage { get; private set; } = string.Empty;
public Version CurrentClientVersion => _connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0);
public DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null;
public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty;
public bool HasVanity => _connectionDto?.HasVanity ?? false;
public string TextColorHex => _connectionDto?.TextColorHex ?? string.Empty;
public string TextGlowColorHex => _connectionDto?.TextGlowColorHex ?? string.Empty;
public bool IsConnected => ServerState == ServerState.Connected;
public bool IsCurrentVersion => (Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0)) >= (_connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0, 0));
public int OnlineUsers => SystemInfoDto.OnlineUsers;
public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected;
public ServerInfo ServerInfo => _connectionDto?.ServerInfo ?? new ServerInfo();
public ServerState ServerState
{
get => _serverState;
private set
{
Logger.LogDebug("New ServerState: {value}, prev ServerState: {_serverState}", value, _serverState);
_serverState = value;
}
}
public SystemInfoDto SystemInfoDto { get; private set; } = new();
public IReadOnlyList<ZoneChatChannelInfoDto> ZoneChatChannels => _zoneChatChannels;
public IReadOnlyList<GroupChatChannelInfoDto> GroupChatChannels => _groupChatChannels;
public string UID => _connectionDto?.User.UID ?? string.Empty;
public event Action? OnConnected;
public async Task<bool> CheckClientHealth()
{
var hub = _lightlessHub;
if (hub is null || !IsConnected)
{
return false;
}
try
{
return await hub.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Client health check failed.");
return false;
}
}
public async Task RefreshChatChannelsAsync()
{
if (_lightlessHub is null || !IsConnected)
return;
await Task.WhenAll(GetZoneChatChannelsAsync(), GetGroupChatChannelsAsync()).ConfigureAwait(false);
}
public async Task<IReadOnlyList<ZoneChatChannelInfoDto>> GetZoneChatChannelsAsync()
{
if (_lightlessHub is null || !IsConnected)
return _zoneChatChannels;
var channels = await _lightlessHub.InvokeAsync<IReadOnlyList<ZoneChatChannelInfoDto>>("GetZoneChatChannels").ConfigureAwait(false);
_zoneChatChannels = channels;
return channels;
}
public async Task<IReadOnlyList<GroupChatChannelInfoDto>> GetGroupChatChannelsAsync()
{
if (_lightlessHub is null || !IsConnected)
return _groupChatChannels;
var channels = await _lightlessHub.InvokeAsync<IReadOnlyList<GroupChatChannelInfoDto>>("GetGroupChatChannels").ConfigureAwait(false);
_groupChatChannels = channels;
return channels;
}
Task<IReadOnlyList<ZoneChatChannelInfoDto>> ILightlessHub.GetZoneChatChannels()
=> _lightlessHub!.InvokeAsync<IReadOnlyList<ZoneChatChannelInfoDto>>("GetZoneChatChannels");
Task<IReadOnlyList<GroupChatChannelInfoDto>> ILightlessHub.GetGroupChatChannels()
=> _lightlessHub!.InvokeAsync<IReadOnlyList<GroupChatChannelInfoDto>>("GetGroupChatChannels");
public async Task CreateConnectionsAsync()
{
if (!_serverManager.ShownCensusPopup)
{
Mediator.Publish(new OpenCensusPopupMessage());
while (!_serverManager.ShownCensusPopup)
{
await Task.Delay(500).ConfigureAwait(false);
}
}
Logger.LogDebug("CreateConnections called");
if (_serverManager.CurrentServer?.FullPause ?? true)
{
Logger.LogInformation("Not recreating Connection, paused");
_connectionDto = null;
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
if (!_serverManager.CurrentServer.UseOAuth2)
{
var secretKey = _serverManager.GetSecretKey(out bool multi);
if (multi)
{
Logger.LogWarning("Multiple secret keys for current character");
_connectionDto = null;
Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.",
NotificationType.Error));
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
if (secretKey.IsNullOrEmpty())
{
Logger.LogWarning("No secret key set for current character");
_connectionDto = null;
await StopConnectionAsync(ServerState.NoSecretKey).ConfigureAwait(false);
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
}
else
{
var oauth2 = _serverManager.GetOAuth2(out bool multi);
if (multi)
{
Logger.LogWarning("Multiple secret keys for current character");
_connectionDto = null;
Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.",
NotificationType.Error));
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
if (!oauth2.HasValue)
{
Logger.LogWarning("No UID/OAuth set for current character");
_connectionDto = null;
await StopConnectionAsync(ServerState.OAuthMisconfigured).ConfigureAwait(false);
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
if (!await _tokenProvider.TryUpdateOAuth2LoginTokenAsync(_serverManager.CurrentServer).ConfigureAwait(false))
{
Logger.LogWarning("OAuth2 login token could not be updated");
_connectionDto = null;
await StopConnectionAsync(ServerState.OAuthLoginTokenStale).ConfigureAwait(false);
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
}
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
Logger.LogInformation("Recreating Connection");
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational,
$"Starting Connection to {_serverManager.CurrentServer.ServerName}")));
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
_connectionCancellationTokenSource?.Dispose();
_connectionCancellationTokenSource = new CancellationTokenSource();
var token = _connectionCancellationTokenSource.Token;
while (ServerState is not ServerState.Connected && !token.IsCancellationRequested)
{
AuthFailureMessage = string.Empty;
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
ServerState = ServerState.Connecting;
try
{
Logger.LogDebug("Building connection");
try
{
_lastUsedToken = await _tokenProvider.GetOrUpdateToken(token).ConfigureAwait(false);
}
catch (LightlessAuthFailureException ex)
{
AuthFailureMessage = ex.Reason;
throw new HttpRequestException("Error during authentication", ex, System.Net.HttpStatusCode.Unauthorized);
}
while (!await _dalamudUtil.GetIsPlayerPresentAsync().ConfigureAwait(false) && !token.IsCancellationRequested)
{
Logger.LogDebug("Player not loaded in yet, waiting");
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
}
if (token.IsCancellationRequested) break;
_lightlessHub = _hubFactory.GetOrCreate(token);
InitializeApiHooks();
await _lightlessHub.StartAsync(token).ConfigureAwait(false);
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
ServerState = ServerState.Connected;
OnConnected?.Invoke();
var currentClientVer = Assembly.GetExecutingAssembly().GetName().Version!;
if (_connectionDto.ServerVersion != ILightlessHub.ApiVersion)
{
if (_connectionDto.CurrentClientVersion > currentClientVer)
{
Mediator.Publish(new NotificationMessage("Client incompatible",
$"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " +
$"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " +
$"This client version is incompatible and will not be able to connect. Please update your Lightless Sync client.",
NotificationType.Error));
}
await StopConnectionAsync(ServerState.VersionMisMatch).ConfigureAwait(false);
return;
}
if (_connectionDto.CurrentClientVersion > currentClientVer)
{
Mediator.Publish(new NotificationMessage("Client outdated",
$"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " +
$"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " +
$"Please keep your Lightless Sync client up-to-date.",
NotificationType.Warning));
}
if (_dalamudUtil.HasModifiedGameFiles)
{
Logger.LogError("Detected modified game files on connection");
if (!_lightlessConfigService.Current.DebugStopWhining)
Mediator.Publish(new NotificationMessage("Modified Game Files detected",
"Dalamud is reporting your FFXIV installation has modified game files. Any mods installed through TexTools will produce this message. " +
"Lightless Sync, Penumbra, and some other plugins assume your FFXIV installation is unmodified in order to work. " +
"Synchronization with pairs/shells can break because of this. Exit the game, open XIVLauncher, click the arrow next to Log In " +
"and select 'repair game files' to resolve this issue. Afterwards, do not install any mods with TexTools. Your plugin configurations will remain, as will mods enabled in Penumbra.",
NotificationType.Error, TimeSpan.FromSeconds(15)));
}
if (_dalamudUtil.IsLodEnabled && !_naggedAboutLod)
{
_naggedAboutLod = true;
Logger.LogWarning("Model LOD is enabled during connection");
if (!_lightlessConfigService.Current.DebugStopWhining)
{
Mediator.Publish(new NotificationMessage("Model LOD is enabled",
"You have \"Use low-detail models on distant objects (LOD)\" enabled. Having model LOD enabled is known to be a reason to cause " +
"random crashes when loading in or rendering modded pairs. Disabling LOD has a very low performance impact. Disable LOD while using Lightless: " +
"Go to XIV Menu -> System Configuration -> Graphics Settings and disable the model LOD option.", NotificationType.Warning, TimeSpan.FromSeconds(15)));
}
}
if (_naggedAboutLod && !_dalamudUtil.IsLodEnabled)
{
_naggedAboutLod = false;
}
await LoadIninitialPairsAsync().ConfigureAwait(false);
await LoadOnlinePairsAsync().ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Logger.LogWarning("Connection attempt cancelled");
return;
}
catch (HttpRequestException ex)
{
Logger.LogWarning(ex, "HttpRequestException on Connection");
if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
await StopConnectionAsync(ServerState.Unauthorized).ConfigureAwait(false);
return;
}
ServerState = ServerState.Reconnecting;
Logger.LogInformation("Failed to establish connection, retrying");
await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false);
}
catch (InvalidOperationException ex)
{
Logger.LogWarning(ex, "InvalidOperationException on connection");
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
return;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Exception on Connection");
Logger.LogInformation("Failed to establish connection, retrying");
await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false);
}
}
}
private bool _naggedAboutLod = false;
public Task CyclePauseAsync(Pair pair)
{
ArgumentNullException.ThrowIfNull(pair);
return CyclePauseAsync(pair.UniqueIdent);
}
public Task CyclePauseAsync(PairUniqueIdentifier ident)
{
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
_ = Task.Run(async () =>
{
var token = timeoutCts.Token;
try
{
if (!_pairCoordinator.Ledger.TryGetEntry(ident, out var entry) || entry is null)
{
Logger.LogWarning("CyclePauseAsync: pair {uid} not found in ledger", ident.UserId);
return;
}
var targetPermissions = entry.SelfPermissions;
targetPermissions.SetPaused(paused: true);
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
var pauseApplied = false;
while (!token.IsCancellationRequested)
{
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
{
if (updated.SelfPermissions == targetPermissions)
{
pauseApplied = true;
entry = updated;
break;
}
}
await Task.Delay(250, token).ConfigureAwait(false);
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
}
if (!pauseApplied)
{
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
return;
}
targetPermissions.SetPaused(paused: false);
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
Logger.LogDebug("CyclePauseAsync completed pause cycle for {uid}", ident.UserId);
}
catch (OperationCanceledException)
{
Logger.LogDebug("CyclePauseAsync cancelled for {uid}", ident.UserId);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "CyclePauseAsync failed for {uid}", ident.UserId);
}
finally
{
timeoutCts.Dispose();
}
}, CancellationToken.None);
return Task.CompletedTask;
}
public async Task PauseAsync(UserData userData)
{
await SetPausedStateAsync(userData, paused: true).ConfigureAwait(false);
}
public async Task UnpauseAsync(UserData userData)
{
await SetPausedStateAsync(userData, paused: false).ConfigureAwait(false);
}
private async Task SetPausedStateAsync(UserData userData, bool paused)
{
var pairIdent = new PairUniqueIdentifier(userData.UID);
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
{
Logger.LogWarning("SetPausedStateAsync: pair {uid} not found in ledger", userData.UID);
return;
}
var permissions = entry.SelfPermissions;
permissions.SetPaused(paused);
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
}
public Task<ConnectionDto> GetConnectionDto() => GetConnectionDtoAsync(true);
public async Task<ConnectionDto> GetConnectionDtoAsync(bool publishConnected)
{
var dto = await _lightlessHub!.InvokeAsync<ConnectionDto>(nameof(GetConnectionDto)).ConfigureAwait(false);
if (publishConnected) Mediator.Publish(new ConnectedMessage(dto));
return dto;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_healthCheckTokenSource?.Cancel();
_ = Task.Run(async () => await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false));
_connectionCancellationTokenSource?.Cancel();
}
private async Task ClientHealthCheckAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
if (_lightlessHub is null)
{
break;
}
await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false);
Logger.LogDebug("Checking Client Health State");
bool requireReconnect = await RefreshTokenAsync(ct).ConfigureAwait(false);
if (requireReconnect) break;
_ = await CheckClientHealth().ConfigureAwait(false);
}
}
private void DalamudUtilOnLogIn()
{
var charaName = _dalamudUtil.GetPlayerName();
var worldId = _dalamudUtil.GetHomeWorldId();
var auth = _serverManager.CurrentServer.Authentications.Find(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId);
if (auth?.AutoLogin ?? false)
{
Logger.LogInformation("Logging into {chara}", charaName);
_ = Task.Run(CreateConnectionsAsync);
}
else
{
Logger.LogInformation("Not logging into {chara}, auto login disabled", charaName);
_ = Task.Run(async () => await StopConnectionAsync(ServerState.NoAutoLogon).ConfigureAwait(false));
}
}
private void DalamudUtilOnLogOut()
{
_ = Task.Run(async () => await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false));
ServerState = ServerState.Offline;
}
private void InitializeApiHooks()
{
if (_lightlessHub == null) return;
Logger.LogDebug("Initializing data");
OnDownloadReady((guid) => _ = Client_DownloadReady(guid));
OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg));
OnReceiveBroadcastPairRequest(dto => _ = Client_ReceiveBroadcastPairRequest(dto));
OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto));
OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto));
OnUserAddClientPair((dto) => _ = Client_UserAddClientPair(dto));
OnUserReceiveCharacterData((dto) => _ = Client_UserReceiveCharacterData(dto));
OnUserRemoveClientPair(dto => _ = Client_UserRemoveClientPair(dto));
OnUserSendOnline(dto => _ = Client_UserSendOnline(dto));
OnUserUpdateOtherPairPermissions(dto => _ = Client_UserUpdateOtherPairPermissions(dto));
OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto));
OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto));
OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto));
OnUserDefaultPermissionUpdate(dto => _ = Client_UserUpdateDefaultPermissions(dto));
OnUpdateUserIndividualPairStatusDto(dto => _ = Client_UpdateUserIndividualPairStatusDto(dto));
OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto));
OnGroupDelete((dto) => _ = Client_GroupDelete(dto));
OnGroupPairChangeUserInfo((dto) => _ = Client_GroupPairChangeUserInfo(dto));
OnGroupPairJoined((dto) => _ = Client_GroupPairJoined(dto));
OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto));
OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto));
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
if (!_initialized)
{
_lightlessHub.On(nameof(Client_ChatReceive), (Func<ChatMessageDto, Task>)Client_ChatReceive);
}
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?.Dispose();
_healthCheckTokenSource = new CancellationTokenSource();
_ = ClientHealthCheckAsync(_healthCheckTokenSource.Token);
_initialized = true;
}
private readonly HashSet<Action<ChatMessageDto>> _chatHandlers = new();
public void RegisterChatMessageHandler(Action<ChatMessageDto> handler)
{
if (_chatHandlers.Add(handler))
{
ChatMessageReceived += handler;
}
}
public void UnregisterChatMessageHandler(Action<ChatMessageDto> handler)
{
if (_chatHandlers.Remove(handler))
{
ChatMessageReceived -= handler;
}
}
private async Task LoadIninitialPairsAsync()
{
foreach (var entry in await GroupsGetAll().ConfigureAwait(false))
{
Logger.LogDebug("Group: {entry}", entry);
_pairCoordinator.HandleGroupFullInfo(entry);
}
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
{
Logger.LogDebug("Individual Pair: {userPair}", userPair);
_pairCoordinator.HandleUserAddPair(userPair);
}
}
private async Task LoadOnlinePairsAsync()
{
CensusDataDto? dto = null;
if (_serverManager.SendCensusData && _lastCensus != null)
{
var world = _dalamudUtil.GetWorldId();
dto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender);
Logger.LogDebug("Attaching Census Data: {data}", dto);
}
foreach (var entry in await UserGetOnlinePairs(dto).ConfigureAwait(false))
{
Logger.LogDebug("Pair online: {pair}", entry);
_pairCoordinator.HandleUserOnline(entry, sendNotification: false);
}
}
private void LightlessHubOnClosed(Exception? arg)
{
_healthCheckTokenSource?.Cancel();
Mediator.Publish(new DisconnectedMessage());
ServerState = ServerState.Offline;
if (arg != null)
{
Logger.LogWarning(arg, "Connection closed");
}
else
{
Logger.LogInformation("Connection closed");
}
}
private async Task LightlessHubOnReconnectedAsync()
{
ServerState = ServerState.Reconnecting;
try
{
InitializeApiHooks();
_connectionDto = await GetConnectionDtoAsync(publishConnected: false).ConfigureAwait(false);
if (_connectionDto.ServerVersion != ILightlessHub.ApiVersion)
{
await StopConnectionAsync(ServerState.VersionMisMatch).ConfigureAwait(false);
return;
}
ServerState = ServerState.Connected;
OnConnected?.Invoke();
await LoadIninitialPairsAsync().ConfigureAwait(false);
await LoadOnlinePairsAsync().ConfigureAwait(false);
Mediator.Publish(new ConnectedMessage(_connectionDto));
}
catch (Exception ex)
{
Logger.LogCritical(ex, "Failure to obtain data after reconnection");
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
}
}
private void LightlessHubOnReconnecting(Exception? arg)
{
_doNotNotifyOnNextInfo = true;
_healthCheckTokenSource?.Cancel();
ServerState = ServerState.Reconnecting;
Logger.LogWarning(arg, "Connection closed... Reconnecting");
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Warning,
$"Connection interrupted, reconnecting to {_serverManager.CurrentServer.ServerName}")));
}
private async Task<bool> RefreshTokenAsync(CancellationToken ct)
{
bool requireReconnect = false;
try
{
var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
if (!string.Equals(token, _lastUsedToken, StringComparison.Ordinal))
{
Logger.LogDebug("Reconnecting due to updated token");
_doNotNotifyOnNextInfo = true;
await CreateConnectionsAsync().ConfigureAwait(false);
requireReconnect = true;
}
}
catch (LightlessAuthFailureException ex)
{
AuthFailureMessage = ex.Reason;
await StopConnectionAsync(ServerState.Unauthorized).ConfigureAwait(false);
requireReconnect = true;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Could not refresh token, forcing reconnect");
_doNotNotifyOnNextInfo = true;
await CreateConnectionsAsync().ConfigureAwait(false);
requireReconnect = true;
}
return requireReconnect;
}
private async Task StopConnectionAsync(ServerState state)
{
ServerState = ServerState.Disconnecting;
Logger.LogInformation("Stopping existing connection");
await _hubFactory.DisposeHubAsync().ConfigureAwait(false);
if (_lightlessHub is not null)
{
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational,
$"Stopping existing connection to {_serverManager.CurrentServer.ServerName}")));
_initialized = false;
if (_healthCheckTokenSource != null)
{
await _healthCheckTokenSource.CancelAsync().ConfigureAwait(false);
}
Mediator.Publish(new DisconnectedMessage());
_lightlessHub = null;
_connectionDto = null;
_zoneChatChannels = Array.Empty<ZoneChatChannelInfoDto>();
_groupChatChannels = Array.Empty<GroupChatChannelInfoDto>();
}
ServerState = state;
}
}
#pragma warning restore MA0040