From a933330418c96b242c2b2d4693522c3bfd322ed0 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Sun, 28 Dec 2025 23:07:45 +0800 Subject: [PATCH] Share location --- LightlessAPI | 2 +- LightlessSync/LightlessSync.csproj | 1 + LightlessSync/Plugin.cs | 1 + LightlessSync/Services/DalamudUtilService.cs | 16 ++- .../Services/LocationShareService.cs | 131 ++++++++++++++++++ LightlessSync/Services/Mediator/Messages.cs | 1 + LightlessSync/UI/Components/DrawUserPair.cs | 102 ++++++++++++++ LightlessSync/UI/DrawEntityFactory.cs | 4 + .../SignalR/ApIController.Functions.Users.cs | 16 +++ .../ApiController.Functions.Callbacks.cs | 13 ++ LightlessSync/WebAPI/SignalR/ApiController.cs | 2 + 11 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 LightlessSync/Services/LocationShareService.cs diff --git a/LightlessAPI b/LightlessAPI index 5656600..852e2a0 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 56566003e0e93bba05dcef49fd3ce23c6a204d81 +Subproject commit 852e2a005f5bfdf3844e057c6ba71de6f5f84ed8 diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 707d2a3..96efb14 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/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 0b93997..96c78f3 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; @@ -86,7 +89,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 })) .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); }); JobData = new(() => @@ -659,7 +663,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) @@ -713,10 +717,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) { diff --git a/LightlessSync/Services/LocationShareService.cs b/LightlessSync/Services/LocationShareService.cs new file mode 100644 index 0000000..71989e5 --- /dev/null +++ b/LightlessSync/Services/LocationShareService.cs @@ -0,0 +1,131 @@ +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; + } + } + + } +} \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 758b9f5..00f8de7 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -135,5 +135,6 @@ 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; #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..b8e58c4 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,41 @@ public class DrawUserPair _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); } UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); + + if (ImGui.BeginMenu(FontAwesomeIcon.Globe.ToIconString() + " 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 Task ToggleLocationSharing(List users, DateTimeOffset expireAt) + { + return _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)); } private void DrawIndividualMenu() @@ -574,6 +612,70 @@ 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 shareLocationOther = expireAt > DateTimeOffset.UtcNow; + var shareColor = shareLocation switch + { + true when shareLocationOther => UIColors.Get("LightlessGreen"), + false when shareLocationOther => UIColors.Get("LightlessBlue"), + _ => UIColors.Get("LightlessYellow"), + }; + + if (shareLocation || shareLocationOther) + { + currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX); + ImGui.SameLine(currentRightSide); + using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationOther)) + _uiSharedService.IconText(shareLocationIcon); + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + if (shareLocationOther) + { + if (_pair.IsOnline) + { + 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("User not onlineㄟ( ▔, ▔ )ㄏ"); + } + } + else + { + ImGui.TextUnformatted("NOT Sharing location with you.(⊙x⊙;)"); + } + ImGui.Separator(); + + if (shareLocation) + { + ImGui.TextUnformatted("Sharing your location.ヾ(•ω•`)o"); + if (expireAt != DateTimeOffset.MaxValue) + { + ImGui.TextUnformatted("Expired at " + expireAt.ToLocalTime().ToString("g")); + } + } + else + { + ImGui.TextUnformatted("NOT sharing your location.(´。_。`)"); + } + ImGui.EndTooltip(); + } + } if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky) { 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..fca42f3 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; + await _lightlessHub!.SendAsync(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