From 6b49c92ef98c0020aaa8b5dc873e40deeba6d9d1 Mon Sep 17 00:00:00 2001 From: defnotken Date: Mon, 29 Dec 2025 08:41:32 -0600 Subject: [PATCH 1/7] Add a timeout to prevent deadlock of application data --- .../PlayerData/Pairs/PairHandlerAdapter.cs | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 82c4a94..bec8322 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -1420,10 +1420,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa } private Task? _pairDownloadTask; - private Task _visibilityGraceTask; private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, - bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) + bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) { var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); try @@ -1577,24 +1576,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa RecordFailure("Handler not available for application", "HandlerUnavailable"); return; } + _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); - var appToken = _applicationCancellationTokenSource?.Token; - while ((!_applicationTask?.IsCompleted ?? false) - && !downloadToken.IsCancellationRequested - && (!appToken?.IsCancellationRequested ?? false)) + if (_applicationTask != null && !_applicationTask.IsCompleted) { - Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName); - await Task.Delay(250).ConfigureAwait(false); + Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName); + + var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token); + + try + { + await _applicationTask.WaitAsync(combinedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Logger.LogWarning("[BASE-{appBase}] Timeout waiting for application task {id} to complete, proceeding anyway", applicationBase, _applicationId); + } + finally + { + timeoutCts.Dispose(); + combinedCts.Dispose(); + } } - if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) + if (downloadToken.IsCancellationRequested) { _forceFullReapply = true; RecordFailure("Application cancelled", "Cancellation"); return; } - _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); var token = _applicationCancellationTokenSource.Token; _applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token); From 27d4da4615c57df034d7321739944c2d0707eebc Mon Sep 17 00:00:00 2001 From: defnotken Date: Mon, 29 Dec 2025 08:47:51 -0600 Subject: [PATCH 2/7] thought a variable was unused. --- LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index bec8322..71cdda7 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -1420,6 +1420,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa } private Task? _pairDownloadTask; + private Task _visibilityGraceTask; private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) From 308c2207359c8e6c0d90b81a06b98f9782012817 Mon Sep 17 00:00:00 2001 From: cake Date: Tue, 30 Dec 2025 02:08:54 +0100 Subject: [PATCH 3/7] Fixed auto prune options locked --- LightlessSync/UI/SyncshellAdminUI.cs | 50 +++++++++++++++------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 0458c05..526b5ae 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -116,7 +116,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var drawList = ImGui.GetWindowDrawList(); var purple = UIColors.Get("LightlessPurple"); - var gradLeft = purple.WithAlpha(0.0f); + var gradLeft = purple.WithAlpha(0.0f); var gradRight = purple.WithAlpha(0.85f); uint colTopLeft = ImGui.ColorConvertFloat4ToU32(gradLeft); @@ -162,7 +162,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var subtitlePos = new Vector2( pMin.X + 12f * scale, - titlePos.Y + titleHeight - 2f * scale); + titlePos.Y + titleHeight - 2f * scale); ImGui.SetCursorScreenPos(subtitlePos); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); @@ -392,25 +392,27 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } UiSharedService.AttachToolTip("When enabled, inactive non-pinned, non-moderator users will be pruned automatically on the server."); - ImGui.SameLine(); - ImGui.SetNextItemWidth(150); - - using (ImRaii.Disabled(!_autoPruneEnabled)) - { - _uiSharedService.DrawCombo( - "Day(s) of inactivity", - [1, 3, 7, 14, 30, 90], - days => $"{days} day(s)", - selected => - { - _autoPruneDays = selected; - SavePruneSettings(); - }, - _autoPruneDays); - } if (!_autoPruneEnabled) { + ImGui.BeginDisabled(); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(150); + _uiSharedService.DrawCombo( + "Day(s) of inactivity (gets checked hourly)", + [0, 1, 3, 7, 14, 30, 90], + (count) => count == 0 ? "2 hours(s)" : count + " day(s)", + selected => + { + _autoPruneDays = selected; + SavePruneSettings(); + }, + _autoPruneDays); + + if (!_autoPruneEnabled) + { + ImGui.EndDisabled(); UiSharedService.ColorTextWrapped( "Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.", ImGuiColors.DalamudGrey); @@ -593,7 +595,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase _uiSharedService.DrawCombo( "Day(s) of inactivity", [0, 1, 3, 7, 14, 30, 90], - (count) => count == 0 ? "15 minute(s)" : count + " day(s)", + (count) => count == 0 ? "2 hours(s)" : count + " day(s)", (selected) => { _pruneDays = selected; @@ -663,8 +665,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var style = ImGui.GetStyle(); float fullW = ImGui.GetContentRegionAvail().X; - float colIdentity = fullW * 0.45f; - float colMeta = fullW * 0.35f; + float colIdentity = fullW * 0.45f; + float colMeta = fullW * 0.35f; float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f; // Header @@ -873,7 +875,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var boolcolor = UiSharedService.GetBoolColor(pair.IsOnline); UiSharedService.ColorText(text, boolcolor); - + if (ImGui.IsItemClicked()) ImGui.SetClipboardText(pair.UserData.AliasOrUID); @@ -1093,6 +1095,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); } + private void SavePruneSettings() { if (_autoPruneDays <= 0) @@ -1100,8 +1103,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase _autoPruneEnabled = false; } - var enabled = _autoPruneEnabled && _autoPruneDays > 0; - var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: enabled, AutoPruneDays: enabled ? _autoPruneDays : 0); + var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: _autoPruneEnabled, AutoPruneDays: _autoPruneDays); try { From 9ea0571e825254e71bb7e4e4df1b24989bc0c943 Mon Sep 17 00:00:00 2001 From: defnotken Date: Tue, 30 Dec 2025 14:29:38 +0000 Subject: [PATCH 4/7] Lower Time out --- LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 71cdda7..b0f2710 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -1583,7 +1583,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName); - var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token); try From c32c89d1a898f11920ee7b50c0d7f62eb026f39c Mon Sep 17 00:00:00 2001 From: defnotken Date: Tue, 30 Dec 2025 08:52:59 -0600 Subject: [PATCH 5/7] Complete Decompression after try. --- LightlessSync/WebAPI/Files/FileDownloadManager.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index 2731619..8aa2b0b 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -502,6 +502,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } } + private void RemoveStatus(string key) + { + lock (_downloadStatusLock) + { + _downloadStatus.Remove(key); + } + } + private async Task DecompressBlockFileAsync( string downloadStatusKey, string blockFilePath, @@ -595,6 +603,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase { Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel); } + finally + { + RemoveStatus(downloadStatusKey); + } } public async Task> InitiateDownloadList( @@ -866,6 +878,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1); Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash); + + RemoveStatus(directDownload.DirectDownloadUrl!); } catch (OperationCanceledException ex) { From bbb337566117efd13e23bee052075de5e2ca4892 Mon Sep 17 00:00:00 2001 From: defnotken Date: Wed, 31 Dec 2025 02:44:31 +0000 Subject: [PATCH 6/7] 2.0.3 staaato --- LightlessSync/LightlessSync.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 707d2a3..f04696c 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -3,7 +3,7 @@ - 2.0.2 + 2.0.3 https://github.com/Light-Public-Syncshells/LightlessClient From 7e61954541e96414e5873351a9b49298b341ef6b Mon Sep 17 00:00:00 2001 From: Tsubasa Date: Wed, 31 Dec 2025 17:31:31 +0000 Subject: [PATCH 7/7] Location Sharing 2.0 (#125) Need: https://git.lightless-sync.org/Lightless-Sync/LightlessServer/pulls/49 Authored-by: Tsubasahane Reviewed-on: https://git.lightless-sync.org/Lightless-Sync/LightlessClient/pulls/125 Reviewed-by: cake Co-authored-by: Tsubasa Co-committed-by: Tsubasa --- LightlessAPI | 2 +- LightlessSync/LightlessSync.csproj | 1 + LightlessSync/Plugin.cs | 1 + LightlessSync/Services/ContextMenuService.cs | 48 +----- LightlessSync/Services/DalamudUtilService.cs | 31 +++- .../Services/LocationShareService.cs | 137 ++++++++++++++++++ LightlessSync/Services/Mediator/Messages.cs | 2 + LightlessSync/UI/Components/DrawUserPair.cs | 110 ++++++++++++++ LightlessSync/UI/DataAnalysisUi.cs | 2 +- LightlessSync/UI/DrawEntityFactory.cs | 4 + .../SignalR/ApIController.Functions.Users.cs | 16 ++ .../ApiController.Functions.Callbacks.cs | 13 ++ LightlessSync/WebAPI/SignalR/ApiController.cs | 2 + 13 files changed, 320 insertions(+), 49 deletions(-) create mode 100644 LightlessSync/Services/LocationShareService.cs diff --git a/LightlessAPI b/LightlessAPI index 5656600..4ecd537 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 56566003e0e93bba05dcef49fd3ce23c6a204d81 +Subproject commit 4ecd5375e63082f44b841bcba38d5dd3f4a2a79b diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index f04696c..938d413 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -37,6 +37,7 @@ + diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 2d46b43..d070831 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -140,6 +140,7 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(sp => new TextureMetadataHelper(sp.GetRequiredService>(), gameData)); diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 3fe893c..024e17b 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -10,7 +10,6 @@ using LightlessSync.UI; using LightlessSync.UI.Services; using LightlessSync.Utils; using LightlessSync.WebAPI; -using Lumina.Excel.Sheets; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -172,9 +171,8 @@ internal class ContextMenuService : IHostedService _logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId); return; } - - var world = GetWorld(target.TargetHomeWorld.RowId); - if (!IsWorldValid(world)) + + if (!IsWorldValid(target.TargetHomeWorld.RowId)) { _logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId); return; @@ -226,9 +224,8 @@ internal class ContextMenuService : IHostedService { if (args.Target is not MenuTargetDefault target) return; - - var world = GetWorld(target.TargetHomeWorld.RowId); - if (!IsWorldValid(world)) + + if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId)) return; try @@ -237,7 +234,7 @@ internal class ContextMenuService : IHostedService if (targetData == null || targetData.Address == nint.Zero) { - _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name); + _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.Value.Name); return; } @@ -252,7 +249,7 @@ internal class ContextMenuService : IHostedService } // Notify in chat when NotificationService is disabled - NotifyInChat($"Pair request sent to {target.TargetName}@{world.Name}.", NotificationType.Info); + NotifyInChat($"Pair request sent to {target.TargetName}@{target.TargetHomeWorld.Value.Name}.", NotificationType.Info); } catch (Exception ex) { @@ -312,37 +309,8 @@ internal class ContextMenuService : IHostedService p.HomeWorld.RowId == target.TargetHomeWorld.RowId); } - private World GetWorld(uint worldId) + private bool IsWorldValid(uint worldId) { - var sheet = _gameData.GetExcelSheet()!; - var luminaWorlds = sheet.Where(x => - { - var dc = x.DataCenter.ValueNullable; - var name = x.Name.ExtractText(); - var internalName = x.InternalName.ExtractText(); - - if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText())) - return false; - - if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName)) - return false; - - if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal)) - return false; - - return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name); - }); - - return luminaWorlds.FirstOrDefault(x => x.RowId == worldId); - } - - private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter); - - private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF; - - public static bool IsWorldValid(World world) - { - var name = world.Name.ToString(); - return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]); + return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId); } } diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 0b93997..b278667 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -1,11 +1,13 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Text; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using LightlessSync.API.Dto.CharaData; @@ -26,6 +28,7 @@ using System.Text; using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; +using Map = Lumina.Excel.Sheets.Map; using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; namespace LightlessSync.Services; @@ -57,6 +60,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private string _lastGlobalBlockReason = string.Empty; private ushort _lastZone = 0; private ushort _lastWorldId = 0; + private uint _lastMapId = 0; private bool _sentBetweenAreas = false; private Lazy _cid; @@ -86,7 +90,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber WorldData = new(() => { return gameData.GetExcelSheet(clientLanguage)! - .Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0]))) + .Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0]) + || w is { RowId: > 1000, Region: 101 or 201 })) .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); }); JobData = new(() => @@ -659,7 +664,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var location = new LocationInfo(); location.ServerId = _playerState.CurrentWorld.RowId; - //location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first + location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; location.TerritoryId = _clientState.TerritoryType; location.MapId = _clientState.MapId; if (houseMan != null) @@ -685,7 +690,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var outside = houseMan->OutdoorTerritory; var house = outside->HouseId; location.WardId = house.WardIndex + 1u; - location.HouseId = (uint)houseMan->GetCurrentPlot() + 1; + //location.HouseId = (uint)houseMan->GetCurrentPlot() + 1; location.DivisionId = houseMan->GetCurrentDivision(); } //_logger.LogWarning(LocationToString(location)); @@ -713,10 +718,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber str += $" - {MapData.Value[(ushort)location.MapId].MapName}"; } - // if (location.InstanceId is not 0) - // { - // str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); - // } + if (location.InstanceId is not 0) + { + str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); + } if (location.WardId is not 0) { @@ -1135,6 +1140,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.Publish(new ZoneSwitchEndMessage()); Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); } + + //Map + if (!_sentBetweenAreas) + { + var mapid = _clientState.MapId; + if (mapid != _lastMapId) + { + _lastMapId = mapid; + Mediator.Publish(new MapChangedMessage(mapid)); + } + } + var localPlayer = _objectTable.LocalPlayer; if (localPlayer != null) diff --git a/LightlessSync/Services/LocationShareService.cs b/LightlessSync/Services/LocationShareService.cs new file mode 100644 index 0000000..38b2834 --- /dev/null +++ b/LightlessSync/Services/LocationShareService.cs @@ -0,0 +1,137 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Dto.CharaData; +using LightlessSync.API.Dto.User; +using LightlessSync.Services.Mediator; +using LightlessSync.WebAPI; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +namespace LightlessSync.Services +{ + public class LocationShareService : DisposableMediatorSubscriberBase + { + private readonly DalamudUtilService _dalamudUtilService; + private readonly ApiController _apiController; + private IMemoryCache _locations = new MemoryCache(new MemoryCacheOptions()); + private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions()); + private CancellationTokenSource _resetToken = new CancellationTokenSource(); + + public LocationShareService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator) + { + _dalamudUtilService = dalamudUtilService; + _apiController = apiController; + + + Mediator.Subscribe(this, (msg) => + { + _resetToken.Cancel(); + _resetToken.Dispose(); + _resetToken = new CancellationTokenSource(); + }); + Mediator.Subscribe(this, (msg) => + { + _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData())); + _ = RequestAllLocation(); + } ); + Mediator.Subscribe(this, UpdateLocationList); + Mediator.Subscribe(this, + msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData()))); + } + + private void UpdateLocationList(LocationSharingMessage msg) + { + if (_locations.TryGetValue(msg.User.UID, out _) && msg.LocationInfo.ServerId is 0) + { + _locations.Remove(msg.User.UID); + return; + } + + if ( msg.LocationInfo.ServerId is not 0 && msg.ExpireAt > DateTime.UtcNow) + { + AddLocationInfo(msg.User.UID, msg.LocationInfo, msg.ExpireAt); + } + } + + private void AddLocationInfo(string uid, LocationInfo location, DateTimeOffset expireAt) + { + var options = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(expireAt) + .AddExpirationToken(new CancellationChangeToken(_resetToken.Token)); + _locations.Set(uid, location, options); + } + + private async Task RequestAllLocation() + { + try + { + var (data, status) = await _apiController.RequestAllLocationInfo().ConfigureAwait(false); + foreach (var dto in data) + { + AddLocationInfo(dto.LocationDto.User.UID, dto.LocationDto.Location, dto.ExpireAt); + } + + foreach (var dto in status) + { + AddStatus(dto.User.UID, dto.ExpireAt); + } + } + catch (Exception e) + { + Logger.LogError(e,"RequestAllLocation error : "); + throw; + } + } + + private void AddStatus(string uid, DateTimeOffset expireAt) + { + var options = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(expireAt) + .AddExpirationToken(new CancellationChangeToken(_resetToken.Token)); + _sharingStatus.Set(uid, expireAt, options); + } + + public string GetUserLocation(string uid) + { + try + { + if (_locations.TryGetValue(uid, out var location)) + { + return _dalamudUtilService.LocationToString(location); + } + return String.Empty; + } + catch (Exception e) + { + Logger.LogError(e,"GetUserLocation error : "); + throw; + } + } + + public DateTimeOffset GetSharingStatus(string uid) + { + try + { + if (_sharingStatus.TryGetValue(uid, out var expireAt)) + { + return expireAt; + } + return DateTimeOffset.MinValue; + } + catch (Exception e) + { + Logger.LogError(e,"GetSharingStatus error : "); + throw; + } + } + + public void UpdateSharingStatus(List users, DateTimeOffset expireAt) + { + foreach (var user in users) + { + AddStatus(user, expireAt); + } + } + + } +} \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 758b9f5..efe3341 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -135,5 +135,7 @@ public record ChatChannelsUpdated : MessageBase; public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase; public record GroupCollectionChangedMessage : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase; +public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase; +public record MapChangedMessage(uint MapId) : MessageBase; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index a5fa953..c8725e2 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -37,6 +37,7 @@ public class DrawUserPair private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly LightlessConfigService _configService; + private readonly LocationShareService _locationShareService; private readonly CharaDataManager _charaDataManager; private readonly PairLedger _pairLedger; private float _menuWidth = -1; @@ -57,6 +58,7 @@ public class DrawUserPair UiSharedService uiSharedService, PlayerPerformanceConfigService performanceConfigService, LightlessConfigService configService, + LocationShareService locationShareService, CharaDataManager charaDataManager, PairLedger pairLedger) { @@ -74,6 +76,7 @@ public class DrawUserPair _uiSharedService = uiSharedService; _performanceConfigService = performanceConfigService; _configService = configService; + _locationShareService = locationShareService; _charaDataManager = charaDataManager; _pairLedger = pairLedger; } @@ -216,6 +219,48 @@ public class DrawUserPair _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); } UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); + + ImGui.SetCursorPosX(10f); + _uiSharedService.IconText(FontAwesomeIcon.Globe); + ImGui.SameLine(); + if (ImGui.BeginMenu("Toggle Location sharing")) + { + if (ImGui.MenuItem("Share for 30 Mins")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddMinutes(30)); + } + + if (ImGui.MenuItem("Share for 1 Hour")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(1)); + } + + if (ImGui.MenuItem("Share for 3 Hours")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(3)); + } + + if (ImGui.MenuItem("Share until manually stop")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MaxValue); + } + + ImGui.Separator(); + if (ImGui.MenuItem("Stop Sharing")) + { + _ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MinValue); + } + ImGui.EndMenu(); + } + } + + private async Task ToggleLocationSharing(List users, DateTimeOffset expireAt) + { + var updated = await _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)).ConfigureAwait(false); + if (updated) + { + _locationShareService.UpdateSharingStatus(users, expireAt); + } } private void DrawIndividualMenu() @@ -574,6 +619,71 @@ public class DrawUserPair var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky(); var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle; + + var shareLocationIcon = FontAwesomeIcon.Globe; + var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID); + var shareLocation = !string.IsNullOrEmpty(location); + var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID); + var shareLocationToOther = expireAt > DateTimeOffset.UtcNow; + var shareColor = shareLocation switch + { + true when shareLocationToOther => UIColors.Get("LightlessGreen"), + true when !shareLocationToOther => UIColors.Get("LightlessBlue"), + _ => UIColors.Get("LightlessYellow"), + }; + + if (shareLocation || shareLocationToOther) + { + currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX); + ImGui.SameLine(currentRightSide); + using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther)) + _uiSharedService.IconText(shareLocationIcon); + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + if (_pair.IsOnline) + { + if (shareLocation) + { + if (!string.IsNullOrEmpty(location)) + { + _uiSharedService.IconText(FontAwesomeIcon.LocationArrow); + ImGui.SameLine(); + ImGui.TextUnformatted(location); + } + else + { + ImGui.TextUnformatted("Location info not updated, reconnect or wait for update."); + } + } + else + { + ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)"); + } + } + else + { + ImGui.TextUnformatted("User not online. (´・ω・`)?"); + } + ImGui.Separator(); + + if (shareLocationToOther) + { + ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o"); + if (expireAt != DateTimeOffset.MaxValue) + { + ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g")); + } + } + else + { + ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄"); + } + ImGui.EndTooltip(); + } + } if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky) { diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 32245d2..a3061a7 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -2183,7 +2183,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase bool toggleClicked = false; if (showToggle) { - var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; + var icon = !isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; Vector2 iconSize; using (_uiSharedService.IconFont.Push()) { diff --git a/LightlessSync/UI/DrawEntityFactory.cs b/LightlessSync/UI/DrawEntityFactory.cs index 3c71f5c..e7bcc87 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -29,6 +29,7 @@ public class DrawEntityFactory private readonly LightlessConfigService _configService; private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; + private readonly LocationShareService _locationShareService; private readonly CharaDataManager _charaDataManager; private readonly SelectTagForPairUi _selectTagForPairUi; private readonly RenamePairTagUi _renamePairTagUi; @@ -53,6 +54,7 @@ public class DrawEntityFactory LightlessConfigService configService, UiSharedService uiSharedService, PlayerPerformanceConfigService playerPerformanceConfigService, + LocationShareService locationShareService, CharaDataManager charaDataManager, SelectTagForSyncshellUi selectTagForSyncshellUi, RenameSyncshellTagUi renameSyncshellTagUi, @@ -72,6 +74,7 @@ public class DrawEntityFactory _configService = configService; _uiSharedService = uiSharedService; _playerPerformanceConfigService = playerPerformanceConfigService; + _locationShareService = locationShareService; _charaDataManager = charaDataManager; _selectTagForSyncshellUi = selectTagForSyncshellUi; _renameSyncshellTagUi = renameSyncshellTagUi; @@ -162,6 +165,7 @@ public class DrawEntityFactory _uiSharedService, _playerPerformanceConfigService, _configService, + _locationShareService, _charaDataManager, _pairLedger); } diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index fdc4719..37b91f9 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -200,5 +200,21 @@ public partial class ApiController await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false); } + + public async Task UpdateLocation(LocationDto locationDto, bool offline = false) + { + if (!IsConnected) return; + await _lightlessHub!.SendAsync(nameof(UpdateLocation), locationDto, offline).ConfigureAwait(false); + } + public async Task<(List, List)> RequestAllLocationInfo() + { + if (!IsConnected) return ([],[]); + return await _lightlessHub!.InvokeAsync<(List, List)>(nameof(RequestAllLocationInfo)).ConfigureAwait(false); + } + public async Task ToggleLocationSharing(LocationSharingToggleDto dto) + { + if (!IsConnected) return false; + return await _lightlessHub!.InvokeAsync(nameof(ToggleLocationSharing), dto).ConfigureAwait(false); + } } #pragma warning restore MA0040 \ No newline at end of file diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 490800f..d0a05f7 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -259,6 +259,13 @@ public partial class ApiController ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData))); return Task.CompletedTask; } + + public Task Client_SendLocationToClient(LocationDto locationDto, DateTimeOffset expireAt) + { + Logger.LogDebug($"{nameof(Client_SendLocationToClient)}: {locationDto.User} {expireAt}"); + ExecuteSafely(() => Mediator.Publish(new LocationSharingMessage(locationDto.User, locationDto.Location, expireAt))); + return Task.CompletedTask; + } public void OnDownloadReady(Action act) { @@ -441,6 +448,12 @@ public partial class ApiController _lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act); } + public void OnReceiveLocation(Action act) + { + if (_initialized) return; + _lightlessHub!.On(nameof(Client_SendLocationToClient), act); + } + private void ExecuteSafely(Action act) { try diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 9639f6f..45705bf 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -606,6 +606,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL 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(); @@ -774,5 +775,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL ServerState = state; } + } #pragma warning restore MA0040