using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Plugin; using Dalamud.Plugin.Services; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Pairs; using LightlessSync.Utils; using LightlessSync.WebAPI; using Lumina.Excel.Sheets; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace LightlessSync.Services; internal class ContextMenuService : IHostedService { private readonly IContextMenu _contextMenu; private readonly IDalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly IClientState _clientState; private readonly PairManager _pairManager; private readonly PairRequestService _pairRequestService; private readonly ApiController _apiController; private readonly IObjectTable _objectTable; public ContextMenuService( IContextMenu contextMenu, IDalamudPluginInterface pluginInterface, IDataManager gameData, ILogger logger, DalamudUtilService dalamudUtil, ApiController apiController, IObjectTable objectTable, LightlessConfigService configService, PairRequestService pairRequestService, PairManager pairManager, IClientState clientState) { _contextMenu = contextMenu; _pluginInterface = pluginInterface; _gameData = gameData; _logger = logger; _dalamudUtil = dalamudUtil; _apiController = apiController; _objectTable = objectTable; _pairManager = pairManager; _pairRequestService = pairRequestService; _clientState = clientState; } public Task StartAsync(CancellationToken cancellationToken) { _contextMenu.OnMenuOpened += OnMenuOpened; return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { _contextMenu.OnMenuOpened -= OnMenuOpened; return Task.CompletedTask; } public void Enable() { _contextMenu.OnMenuOpened += OnMenuOpened; _logger.LogDebug("Context menu enabled."); } public void Disable() { _contextMenu.OnMenuOpened -= OnMenuOpened; _logger.LogDebug("Context menu disabled."); } private void OnMenuOpened(IMenuOpenedArgs args) { if (!_pluginInterface.UiBuilder.ShouldModifyUi) return; if (args.AddonName != null) return; //Check if target is not menutargetdefault. if (args.Target is not MenuTargetDefault target) return; //Check if name or target id isnt null/zero if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0) return; //Check if it is a real target. IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); if (targetData == null || targetData.Address == nint.Zero) return; //Check if user is paired or is own. if (VisibleUserIds.Any(u => u == target.TargetObjectId) || _clientState.LocalPlayer.GameObjectId == target.TargetObjectId) return; //Check if in PVP or GPose if (_clientState.IsPvPExcludingDen || _clientState.IsGPosing) return; //Check for valid world. var world = GetWorld(target.TargetHomeWorld.RowId); if (!IsWorldValid(world)) return; args.AddMenuItem(new MenuItem { Name = "Send Pair Request", PrefixChar = 'L', UseDefaultPrefix = false, PrefixColor = 708, OnClicked = async _ => await HandleSelection(args).ConfigureAwait(false) }); } private async Task HandleSelection(IMenuArgs args) { if (args.Target is not MenuTargetDefault target) return; var world = GetWorld(target.TargetHomeWorld.RowId); if (!IsWorldValid(world)) return; try { IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); if (targetData == null || targetData.Address == nint.Zero) { _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name); return; } var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(receiverCid)) { _pairRequestService.RemoveRequest(receiverCid); } } catch (Exception ex) { _logger.LogError(ex, "Error sending pair request."); } } private HashSet VisibleUserIds => [.. _pairManager.GetOnlineUserPairs() .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) .Select(u => (ulong)u.PlayerCharacterId)]; private IPlayerCharacter? GetPlayerFromObjectTable(MenuTargetDefault target) { return _objectTable .OfType() .FirstOrDefault(p => string.Equals(p.Name.TextValue, target.TargetName, StringComparison.OrdinalIgnoreCase) && p.HomeWorld.RowId == target.TargetHomeWorld.RowId); } private World GetWorld(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 bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId)); public static bool IsWorldValid(World world) { var name = world.Name.ToString(); return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]); } }