Files
LightlessClient/LightlessSync/WebAPI/SignalR/ApiController.cs
defnotken 72a62b7449
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
2.1.0 (#123)
# Patchnotes 2.1.0
The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update.

We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which:

# Location Sharing (Big shout out to @tsubasahane for bringing this feature)

- Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)

[1]

# Model Optimization (Mesh Decimating)
 - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>)
 - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>)
 - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking.
 - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>)
+ ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE  **

[2]

# Animation (PAP) Validation (Safer animations)
 - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>)
 - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>)
 - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>)

# UI Changes (Thanks to @kyuwu for UI Changes)
- The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>)

[3]

- Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>)
- The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>)
- Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>)

# LightFinder / ShellFinder
- UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does.  [#127](<#127>)

[4]

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: celine <aaa@aaa.aaa>
Co-authored-by: celine <celine@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasahane <wozaiha@gmail.com>
Co-authored-by: cake <cake@noreply.git.lightless-sync.org>
Reviewed-on: #123
2026-01-20 19:43:00 +00:00

781 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));
OnReceiveLocation((dto, time) => _ = Client_SendLocationToClient(dto, time));
_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