From 70745613e13cb27dd7e90d30a811bd8b1f28d965 Mon Sep 17 00:00:00 2001 From: Tsubasahane Date: Sat, 27 Dec 2025 19:57:21 +0800 Subject: [PATCH] Location Sharing --- LightlessAPI | 2 +- LightlessSync/Plugin.cs | 1 + LightlessSync/Services/DalamudUtilService.cs | 13 ++-- .../Services/LocationShareService.cs | 78 +++++++++++++++++++ LightlessSync/Services/Mediator/Messages.cs | 2 + .../UI/Components/DrawFolderGroup.cs | 8 ++ LightlessSync/UI/Components/DrawUserPair.cs | 75 ++++++++++++++++++ LightlessSync/UI/DrawEntityFactory.cs | 4 + LightlessSync/UI/PermissionWindowUI.cs | 20 +++++ .../SignalR/ApIController.Functions.Users.cs | 12 +++ .../ApiController.Functions.Callbacks.cs | 13 ++++ LightlessSync/WebAPI/SignalR/ApiController.cs | 2 + 12 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 LightlessSync/Services/LocationShareService.cs diff --git a/LightlessAPI b/LightlessAPI index 5656600..fdd492a 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 56566003e0e93bba05dcef49fd3ce23c6a204d81 +Subproject commit fdd492a8f478949d910ed0efd3e4a3ca3312ed9c diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index cd57c34..4131e40 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -137,6 +137,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 ef6fe7a..36ce98b 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; @@ -25,6 +27,7 @@ using System.Runtime.CompilerServices; using System.Text; 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; @@ -588,7 +591,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) @@ -642,10 +645,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..0d3f5dc --- /dev/null +++ b/LightlessSync/Services/LocationShareService.cs @@ -0,0 +1,78 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Dto.CharaData; +using LightlessSync.API.Dto.User; +using LightlessSync.Services.Mediator; +using LightlessSync.WebAPI; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Services +{ + public class LocationShareService : DisposableMediatorSubscriberBase + { + private readonly DalamudUtilService _dalamudUtilService; + private readonly ApiController _apiController; + private Dictionary _locations = []; + + public LocationShareService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator) + { + _dalamudUtilService = dalamudUtilService; + _apiController = apiController; + + + Mediator.Subscribe(this, (msg) => _locations.Clear()); + 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(LocationMessage msg) + { + if (_locations.ContainsKey(msg.Uid) && msg.LocationInfo.ServerId is 0) + { + _locations.Remove(msg.Uid); + return; + } + + if ( msg.LocationInfo.ServerId is not 0 && !_locations.TryAdd(msg.Uid, msg.LocationInfo)) + { + _locations[msg.Uid] = msg.LocationInfo; + } + } + + private async Task RequestAllLocation() + { + try + { + var data = await _apiController.RequestAllLocationInfo().ConfigureAwait(false); + _locations = data.ToDictionary(x => x.user.UID, x => x.location, StringComparer.Ordinal); + } + catch (Exception e) + { + Logger.LogError(e,"RequestAllLocation error : "); + throw; + } + } + + 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; + } + } + } +} \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 758b9f5..be0c06b 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 LocationMessage(string Uid, LocationInfo LocationInfo) : 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/DrawFolderGroup.cs b/LightlessSync/UI/Components/DrawFolderGroup.cs index c39326c..277115a 100644 --- a/LightlessSync/UI/Components/DrawFolderGroup.cs +++ b/LightlessSync/UI/Components/DrawFolderGroup.cs @@ -131,6 +131,7 @@ public class DrawFolderGroup : DrawFolderBase bool disableSounds = perm.IsDisableSounds(); bool disableAnims = perm.IsDisableAnimations(); bool disableVfx = perm.IsDisableVFX(); + bool shareLocation = perm.IsSharingLocation(); if ((_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations() != disableAnims || _groupFullInfoDto.GroupPermissions.IsPreferDisableSounds() != disableSounds @@ -164,6 +165,13 @@ public class DrawFolderGroup : DrawFolderBase _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); ImGui.CloseCurrentPopup(); } + + if (_uiSharedService.IconTextButton(!shareLocation ? FontAwesomeIcon.Globe : FontAwesomeIcon.StopCircle, !shareLocation ? "Share your location to all users in Syncshell" : "STOP Share your location to all users in Syncshell", menuWidth, true)) + { + perm.SetShareLocation(!shareLocation); + _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); + ImGui.CloseCurrentPopup(); + } if (IsModerator || IsOwner) { diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index 8e03ae4..5f3d300 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,17 @@ public class DrawUserPair _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); } UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); + + var isShareingLocation = _pair.UserPair!.OwnPermissions.IsSharingLocation(); + string isShareingLocationText = isShareingLocation ? "Disable location sharing" : "Enable location sharing"; + var isShareingLocationIcon = isShareingLocation ? FontAwesomeIcon.StopCircle : FontAwesomeIcon.Globe; + if (_uiSharedService.IconTextButton(isShareingLocationIcon, isShareingLocationText, _menuWidth, true)) + { + var permissions = _pair.UserPair.OwnPermissions; + permissions.SetShareLocation(!isShareingLocation); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); + } + UiSharedService.AttachToolTip("Changes location sharing permissions with this user." + (individual ? individualText : string.Empty)); } private void DrawIndividualMenu() @@ -567,6 +581,7 @@ public class DrawUserPair : UiSharedService.TooltipSeparator + "Hold CTRL to enable preferred permissions while pausing." + Environment.NewLine + "This will leave this pair paused even if unpausing syncshells including this pair.")) : "Resume pairing with " + _pair.UserData.AliasOrUID); + //Location sharing if (_pair.IsPaired) { var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); @@ -574,6 +589,66 @@ 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 shareLocation = _pair.UserPair?.OwnPermissions.IsSharingLocation() ?? false; + var shareLocationOther = _pair.UserPair?.OtherPermissions.IsSharingLocation() ?? false; + 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) + { + var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID); + 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 waiting for zone-changing."); + } + } + 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"); + } + 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..e810a29 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -28,6 +28,7 @@ public class DrawEntityFactory private readonly ServerConfigurationManager _serverConfigurationManager; private readonly LightlessConfigService _configService; private readonly UiSharedService _uiSharedService; + private readonly LocationShareService _locationShareService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly CharaDataManager _charaDataManager; private readonly SelectTagForPairUi _selectTagForPairUi; @@ -52,6 +53,7 @@ public class DrawEntityFactory ServerConfigurationManager serverConfigurationManager, LightlessConfigService configService, UiSharedService uiSharedService, + LocationShareService locationShareService, PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager, SelectTagForSyncshellUi selectTagForSyncshellUi, @@ -71,6 +73,7 @@ public class DrawEntityFactory _serverConfigurationManager = serverConfigurationManager; _configService = configService; _uiSharedService = uiSharedService; + _locationShareService = locationShareService; _playerPerformanceConfigService = playerPerformanceConfigService; _charaDataManager = charaDataManager; _selectTagForSyncshellUi = selectTagForSyncshellUi; @@ -162,6 +165,7 @@ public class DrawEntityFactory _uiSharedService, _playerPerformanceConfigService, _configService, + _locationShareService, _charaDataManager, _pairLedger); } diff --git a/LightlessSync/UI/PermissionWindowUI.cs b/LightlessSync/UI/PermissionWindowUI.cs index 5dee098..9d88547 100644 --- a/LightlessSync/UI/PermissionWindowUI.cs +++ b/LightlessSync/UI/PermissionWindowUI.cs @@ -43,6 +43,7 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase var disableSounds = _ownPermissions.IsDisableSounds(); var disableAnimations = _ownPermissions.IsDisableAnimations(); var disableVfx = _ownPermissions.IsDisableVFX(); + var shareLocation = _ownPermissions.IsSharingLocation(); var style = ImGui.GetStyle(); var indentSize = ImGui.GetFrameHeight() + style.ItemSpacing.X; @@ -70,6 +71,7 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase var otherDisableSounds = otherPerms.IsDisableSounds(); var otherDisableAnimations = otherPerms.IsDisableAnimations(); var otherDisableVFX = otherPerms.IsDisableVFX(); + var otherShareLocation = otherPerms.IsSharingLocation(); using (ImRaii.PushIndent(indentSize, false)) { @@ -124,6 +126,24 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase ImGui.AlignTextToFramePadding(); ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherDisableVFX ? "not " : string.Empty) + "disabled VFX sync with you"); } + + if (ImGui.Checkbox("Enable location Sharing", ref shareLocation)) + { + _ownPermissions.SetShareLocation(shareLocation); + } + _uiSharedService.DrawHelpText("Enable location sharing will only effect your side." + UiSharedService.TooltipSeparator + + "Note: this is NOT bidirectional, you can choose to share even others dont share with you."); + using (ImRaii.PushIndent(indentSize, false)) + { + _uiSharedService.BooleanToColoredIcon(shareLocation, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text((!shareLocation ? "Not" : string.Empty) + "sharing location with " + Pair.UserData.AliasOrUID + " ."); + +#if DEBUG + _uiSharedService.BooleanToColoredIcon(otherShareLocation, true); +#endif + } ImGuiHelpers.ScaledDummy(0.5f); ImGui.Separator(); diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index fdc4719..6cd704f 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -200,5 +200,17 @@ 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> RequestAllLocationInfo() + { + if (!IsConnected) return []; + return await _lightlessHub!.InvokeAsync>(nameof(RequestAllLocationInfo)).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..01abc88 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) + { + Logger.LogDebug($"{nameof(Client_SendLocationToClient)}: {locationDto.user}"); + ExecuteSafely(() => Mediator.Publish(new LocationMessage(locationDto.user.UID, locationDto.location))); + return Task.CompletedTask; + } public void OnDownloadReady(Action act) { @@ -440,6 +447,12 @@ public partial class ApiController if (_initialized) return; _lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act); } + + public void OnReciveLocation(Action act) + { + if (_initialized) return; + _lightlessHub!.On(nameof(Client_SendLocationToClient), act); + } private void ExecuteSafely(Action act) { diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index 9639f6f..a814758 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -606,6 +606,8 @@ 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)); + OnReciveLocation(dto => _ = Client_SendLocationToClient(dto)); + _healthCheckTokenSource?.Cancel(); _healthCheckTokenSource?.Dispose();