using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Plugin; using Dalamud.Plugin.Services; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; using LightlessSync.Utils; using LightlessSync.WebAPI; using Lumina.Excel.Sheets; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Linq; namespace LightlessSync.UI; internal class ContextMenu : IHostedService { private readonly IContextMenu _contextMenu; private readonly IDalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly LightlessConfigService _configService; private readonly ApiController _apiController; private readonly IObjectTable _objectTable; private static readonly string[] ValidAddons = new[] { null, "PartyMemberList", "FriendList", "FreeCompany", "LinkShell", "CrossWorldLinkshell", "_PartyList", "ChatLog", "LookingForGroup", "BlackList", "ContentMemberList", "SocialList", "ContactList", "BeginnerChatList", "MuteList" }; public ContextMenu( IContextMenu contextMenu, IDalamudPluginInterface pluginInterface, IDataManager gameData, ILogger logger, DalamudUtilService dalamudUtil, ApiController apiController, IObjectTable objectTable, LightlessConfigService configService) { _contextMenu = contextMenu; _pluginInterface = pluginInterface; _gameData = gameData; _logger = logger; _dalamudUtil = dalamudUtil; _apiController = apiController; _objectTable = objectTable; _configService = configService; } 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 (!ValidAddons.Contains(args.AddonName, StringComparer.Ordinal)) return; if (args.Target is not MenuTargetDefault target) return; if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0) return; 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) }); } 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 { var targetData = _objectTable .OfType() .FirstOrDefault(p => string.Equals(p.Name.TextValue, target.TargetName, StringComparison.OrdinalIgnoreCase) && p.HomeWorld.RowId == target.TargetHomeWorld.RowId); if (targetData == null || targetData.Address == IntPtr.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); } catch (Exception ex) { _logger.LogError(ex, "Error sending pair request."); } } 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]); } }