323 lines
12 KiB
C#
323 lines
12 KiB
C#
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
using Dalamud.Game.Gui.ContextMenu;
|
|
using Dalamud.Plugin;
|
|
using Dalamud.Plugin.Services;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.LightlessConfiguration.Models;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.WebAPI;
|
|
using Lumina.Excel.Sheets;
|
|
using LightlessSync.UI.Services;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using LightlessSync.UI;
|
|
using LightlessSync.Services.LightFinder;
|
|
|
|
namespace LightlessSync.Services;
|
|
|
|
internal class ContextMenuService : IHostedService
|
|
{
|
|
private readonly IContextMenu _contextMenu;
|
|
private readonly IDalamudPluginInterface _pluginInterface;
|
|
private readonly IDataManager _gameData;
|
|
private readonly ILogger<ContextMenuService> _logger;
|
|
private readonly DalamudUtilService _dalamudUtil;
|
|
private readonly IClientState _clientState;
|
|
private readonly PairUiService _pairUiService;
|
|
private readonly PairRequestService _pairRequestService;
|
|
private readonly ApiController _apiController;
|
|
private readonly IObjectTable _objectTable;
|
|
private readonly LightlessConfigService _configService;
|
|
private readonly LightFinderScannerService _broadcastScannerService;
|
|
private readonly LightFinderService _broadcastService;
|
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
|
private readonly LightlessMediator _mediator;
|
|
|
|
private const int _lightlessPrefixColor = 708;
|
|
|
|
public ContextMenuService(
|
|
IContextMenu contextMenu,
|
|
IDalamudPluginInterface pluginInterface,
|
|
IDataManager gameData,
|
|
ILogger<ContextMenuService> logger,
|
|
DalamudUtilService dalamudUtil,
|
|
ApiController apiController,
|
|
IObjectTable objectTable,
|
|
LightlessConfigService configService,
|
|
PairRequestService pairRequestService,
|
|
PairUiService pairUiService,
|
|
IClientState clientState,
|
|
LightFinderScannerService broadcastScannerService,
|
|
LightFinderService broadcastService,
|
|
LightlessProfileManager lightlessProfileManager,
|
|
LightlessMediator mediator)
|
|
{
|
|
_contextMenu = contextMenu;
|
|
_pluginInterface = pluginInterface;
|
|
_gameData = gameData;
|
|
_logger = logger;
|
|
_dalamudUtil = dalamudUtil;
|
|
_apiController = apiController;
|
|
_objectTable = objectTable;
|
|
_configService = configService;
|
|
_pairUiService = pairUiService;
|
|
_pairRequestService = pairRequestService;
|
|
_clientState = clientState;
|
|
_broadcastScannerService = broadcastScannerService;
|
|
_broadcastService = broadcastService;
|
|
_lightlessProfileManager = lightlessProfileManager;
|
|
_mediator = mediator;
|
|
}
|
|
|
|
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)
|
|
{
|
|
var addonName = args.AddonName;
|
|
_logger.LogTrace("Context menu addon name: {AddonName}", addonName);
|
|
return;
|
|
}
|
|
|
|
if (args.Target is not MenuTargetDefault target)
|
|
{
|
|
_logger.LogTrace("Context menu target is not MenuTargetDefault.");
|
|
return;
|
|
}
|
|
|
|
_logger.LogTrace("Context menu opened for target: {Target}", target.TargetName ?? "null");
|
|
|
|
if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0)
|
|
{
|
|
_logger.LogTrace("Context menu target has invalid data: Name='{TargetName}', ObjectId={TargetObjectId}, HomeWorldId={TargetHomeWorldId}", target.TargetName, target.TargetObjectId, target.TargetHomeWorld.RowId);
|
|
return;
|
|
}
|
|
|
|
IPlayerCharacter? targetData = GetPlayerFromObjectTable(target);
|
|
if (targetData == null || targetData.Address == nint.Zero || _objectTable.LocalPlayer == null)
|
|
{
|
|
_logger.LogTrace("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.RowId);
|
|
return;
|
|
}
|
|
|
|
var snapshot = _pairUiService.GetSnapshot();
|
|
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
|
p.IsVisible &&
|
|
p.PlayerCharacterId != uint.MaxValue &&
|
|
p.PlayerCharacterId == target.TargetObjectId);
|
|
|
|
if (pair is not null)
|
|
{
|
|
_logger.LogTrace("Target player {TargetName}@{World} is already paired, adding existing pair context menu.", target.TargetName, target.TargetHomeWorld.RowId);
|
|
|
|
pair.AddContextMenu(args);
|
|
if (!pair.IsDirectlyPaired)
|
|
{
|
|
_logger.LogTrace("Target player {TargetName}@{World} is not directly paired, add direct pair menu item", target.TargetName, target.TargetHomeWorld.RowId);
|
|
AddDirectPairMenuItem(args);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
_logger.LogTrace("Target player {TargetName}@{World} is not paired, adding direct pair request context menu.", target.TargetName, target.TargetHomeWorld.RowId);
|
|
|
|
//Check if user is directly paired or is own.
|
|
if (VisibleUserIds.Any(u => u == target.TargetObjectId) || _objectTable.LocalPlayer?.GameObjectId == target.TargetObjectId || !_configService.Current.EnableRightClickMenus)
|
|
{
|
|
_logger.LogTrace("Target player {TargetName}@{World} is already paired or is self, or right-click menus are disabled.", target.TargetName, target.TargetHomeWorld.RowId);
|
|
return;
|
|
}
|
|
|
|
if (_clientState.IsPvPExcludingDen || _clientState.IsGPosing)
|
|
{
|
|
_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))
|
|
{
|
|
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
|
|
return;
|
|
}
|
|
|
|
string? targetHashedCid = null;
|
|
if (_broadcastService.IsBroadcasting)
|
|
{
|
|
targetHashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(targetHashedCid) && CanOpenLightfinderProfile(targetHashedCid))
|
|
{
|
|
var hashedCid = targetHashedCid;
|
|
UiSharedService.AddContextMenuItem(args, name: "Open Lightless Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => HandleLightfinderProfileSelection(hashedCid));
|
|
}
|
|
|
|
AddDirectPairMenuItem(args);
|
|
}
|
|
|
|
private void AddDirectPairMenuItem(IMenuOpenedArgs args)
|
|
{
|
|
UiSharedService.AddContextMenuItem(
|
|
args,
|
|
name: "Send Direct Pair Request",
|
|
prefixChar: 'L',
|
|
colorMenuItem: _lightlessPrefixColor,
|
|
onClick: () => HandleSelection(args));
|
|
}
|
|
|
|
private HashSet<ulong> VisibleUserIds =>
|
|
[.. _pairUiService.GetSnapshot().PairsByUid.Values
|
|
.Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue)
|
|
.Select(p => (ulong)p.PlayerCharacterId)];
|
|
|
|
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().GetBlake3Hash();
|
|
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
|
|
|
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
|
await _apiController.TryPairWithContentId(receiverCid).ConfigureAwait(false);
|
|
if (!string.IsNullOrWhiteSpace(receiverCid))
|
|
{
|
|
_pairRequestService.RemoveRequest(receiverCid);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error sending pair request.");
|
|
}
|
|
}
|
|
|
|
private async Task HandleLightfinderProfileSelection(string hashedCid)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(hashedCid))
|
|
return;
|
|
|
|
if (!_broadcastService.IsBroadcasting)
|
|
{
|
|
Notify("Lightfinder inactive", "Enable Lightfinder to open broadcaster profiles.", NotificationType.Warning, 6);
|
|
return;
|
|
}
|
|
|
|
if (!_broadcastScannerService.BroadcastCache.TryGetValue(hashedCid, out var entry) || !entry.IsBroadcasting || entry.ExpiryTime <= DateTime.UtcNow)
|
|
{
|
|
Notify("Broadcaster unavailable", "That player is not currently using Lightfinder.", NotificationType.Info, 5);
|
|
return;
|
|
}
|
|
|
|
var result = await _lightlessProfileManager.GetLightfinderProfileAsync(hashedCid).ConfigureAwait(false);
|
|
if (result == null)
|
|
{
|
|
Notify("Profile unavailable", "Unable to load Lightless profile for that player.", NotificationType.Error, 6);
|
|
return;
|
|
}
|
|
|
|
_mediator.Publish(new OpenLightfinderProfileMessage(result.Value.User, result.Value.ProfileData, hashedCid));
|
|
}
|
|
|
|
private void Notify(string title, string message, NotificationType type, double durationSeconds)
|
|
{
|
|
_mediator.Publish(new NotificationMessage(title, message, type, TimeSpan.FromSeconds(durationSeconds)));
|
|
}
|
|
|
|
private bool CanOpenLightfinderProfile(string hashedCid)
|
|
{
|
|
if (!_broadcastService.IsBroadcasting)
|
|
return false;
|
|
|
|
if (!_broadcastScannerService.BroadcastCache.TryGetValue(hashedCid, out var entry))
|
|
return false;
|
|
|
|
return entry.IsBroadcasting && entry.ExpiryTime > DateTime.UtcNow;
|
|
}
|
|
|
|
private IPlayerCharacter? GetPlayerFromObjectTable(MenuTargetDefault target)
|
|
{
|
|
return _objectTable
|
|
.OfType<IPlayerCharacter>()
|
|
.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<World>()!;
|
|
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]);
|
|
}
|
|
}
|