feat(payload-handler): wire DrawPlayerPopup + FindCharacterForPayload (E3)

E3 fills the player-payload right-click menu. DrawPlayerPopup migrates
1:1 from v1.5.6 with all §4.2/§6.9 substitutions:

- Tell-prefix builds via _inputBar.SetPendingMessage (single string build)
  + _inputBar.Activate = true (replaces v1.5.6's 3x LogWindow.Chat
  incremental writes + LogWindow.Activate flip)
- Channel-switch routes through _mainWindow.ActiveTab?.CurrentChannel?
  .SetChannel(channel) (per §6.3, ActiveTab is public getter on
  MainWindow per v1.7.0 refactor)
- SendFriendRequest / AddToBlacklist / AddToMuteList / AddToTermsList
  / SetEurekaTellChannel all route through injected _functions
- Player-name renderings route through _chunkRenderer.DrawChunks

FindCharacterForPayload migrates 1:1 (pure helper, Plugin.ObjectTable
static stays as-is).

Replaces E2's TODO(E3) marker in DrawPopups' PlayerPayload case with
real DrawPlayerPopup(chunk, player) call.

E4 will wire Item/EventItem/Status/Uri popup bodies; E5 the Hover/Click
bodies; E6 MoveTooltip.
This commit is contained in:
2026-05-27 12:50:23 +02:00
parent 63f5a28834
commit abc0617d58
+196 -1
View File
@@ -1,5 +1,6 @@
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiNotification;
@@ -85,7 +86,7 @@ internal sealed class PayloadHandler
switch (payload)
{
case PlayerPayload player:
// TODO(E3): DrawPlayerPopup(chunk, player);
DrawPlayerPopup(chunk, player);
drawn = true;
break;
case ItemPayload item:
@@ -217,6 +218,200 @@ internal sealed class PayloadHandler
.Aggregate(string.Concat);
}
private void DrawPlayerPopup(Chunk chunk, PlayerPayload player)
{
// Possible that GMs return a null payload
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (player == null)
return;
var world = player.World;
if (chunk.Message?.Code.Type == ChatType.FreeCompanyLoginLogout)
if (Plugin.PlayerState.HomeWorld.IsValid)
world = Plugin.PlayerState.HomeWorld;
var name = new List<Chunk> { new TextChunk(ChunkSource.None, null, player.PlayerName) };
if (world.Value.IsPublic)
{
name.AddRange([
new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld),
new TextChunk(ChunkSource.None, null, world.Value.Name.ExtractText()),
]);
}
_chunkRenderer.DrawChunks(name, false);
ImGui.Separator();
var validContentId = chunk.Message?.ContentId is not (null or 0);
if (ImGui.Selectable(Language.Context_SendTell))
{
// Eureka, Bozja and Occult need special handling as tells work different
if (!Sheets.IsInForay())
{
// §6.9: build as single string then hand off; replaces v1.5.6's
// incremental LogWindow.Chat += ... pattern
var builder = $"/tell {player.PlayerName}";
if (world.Value.IsPublic)
builder += $"@{world.Value.Name}";
builder += " ";
_inputBar.SetPendingMessage(builder);
}
else if (validContentId)
{
_functions.Chat.SetEurekaTellChannel(
player.PlayerName,
world.Value.Name.ToString(),
(ushort)world.RowId,
0,
chunk.Message!.ContentId,
0,
false
);
}
_inputBar.Activate = true;
}
if (world.Value.IsPublic)
{
var party = Plugin.PartyList;
var leader = party[(int)party.PartyLeaderIndex]?.ContentId;
var isLeader = party.Length == 0 || Plugin.PlayerState.ContentId == leader;
var member = party.FirstOrDefault(member =>
member.Name.TextValue == player.PlayerName && member.World.RowId == world.RowId
);
var isInParty = member != null;
var inInstance = GameFunctions.GameFunctions.IsInInstance();
var inPartyInstance =
Sheets
.TerritorySheet.GetRow(Plugin.ClientState.TerritoryType)
.TerritoryIntendedUse.RowId
is (41 or 47 or 48 or 52 or 53 or 61);
if (isLeader)
{
if (!isInParty)
{
if (inInstance && inPartyInstance)
{
if (validContentId && ImGui.Selectable(Language.Context_InviteToParty))
GameFunctions.Party.InviteInInstance(chunk.Message!.ContentId);
}
else if (!inInstance)
{
using var menu = ImRaii.Menu(Language.Context_InviteToParty);
if (menu.Success)
{
if (ImGui.Selectable(Language.Context_InviteToParty_SameWorld))
GameFunctions.Party.InviteSameWorld(
player.PlayerName,
(ushort)world.RowId,
chunk.Message?.ContentId ?? 0
);
if (
validContentId
&& ImGui.Selectable(Language.Context_InviteToParty_DifferentWorld)
)
GameFunctions.Party.InviteOtherWorld(
chunk.Message!.ContentId,
(ushort)world.RowId
);
}
}
}
if (isInParty && member != null && (!inInstance || (inInstance && inPartyInstance)))
{
if (ImGui.Selectable(Language.Context_Promote))
GameFunctions.Party.Promote(player.PlayerName, member.ContentId);
if (ImGui.Selectable(Language.Context_KickFromParty))
GameFunctions.Party.Kick(player.PlayerName, member.ContentId);
}
}
var isFriend = GameFunctions
.GameFunctions.GetFriends()
.Any(friend =>
friend.NameString == player.PlayerName && friend.HomeWorld == world.RowId
);
if (!isFriend && ImGui.Selectable(Language.Context_SendFriendRequest))
_functions.SendFriendRequest(player.PlayerName, (ushort)world.RowId);
using (var menuBlockFunctions = ImRaii.Menu(Language.Context_BlockFunctions))
{
if (menuBlockFunctions.Success)
{
if (ImGui.Selectable(Language.Context_AddToBlacklist))
_functions.AddToBlacklist(player.PlayerName, (ushort)world.RowId);
if (chunk.Message != null)
{
var message = chunk.Message;
if (
message.AccountId != 0
&& ImGui.Selectable(Language.Context_AddToMuteList)
)
_functions.AddToMuteList(
message.AccountId,
message.ContentId,
player.PlayerName,
(short)world.RowId
);
if (ImGui.Selectable(Language.Context_AddToTermsFilter))
_functions.AddToTermsList(message.ContentSource);
}
}
}
if (
GameFunctions.GameFunctions.IsMentor()
&& ImGui.Selectable(Language.Context_InviteToNoviceNetwork)
)
GameFunctions.Context.InviteToNoviceNetwork(player.PlayerName, (ushort)world.RowId);
}
var inputChannel = chunk.Message?.Code.Type.ToInputChannel();
if (inputChannel != null && ImGui.Selectable(Language.Context_ReplyInSelectedChatMode))
{
// §6.3: route channel-switch through MainWindow's active tab
_mainWindow.ActiveTab?.CurrentChannel?.SetChannel(inputChannel.Value);
_inputBar.Activate = true;
}
if (ImGui.Selectable(Language.Context_Target) && FindCharacterForPayload(player) is { } obj)
Plugin.TargetManager.Target = obj;
if (validContentId && ImGui.Selectable(Language.Context_AdventurerPlate))
if (!GameFunctions.GameFunctions.TryOpenAdventurerPlate(chunk.Message!.ContentId))
WrapperUtil.AddNotification(
Language.Context_AdventurerPlateError,
NotificationType.Warning
);
}
private IPlayerCharacter? FindCharacterForPayload(PlayerPayload payload)
{
foreach (var obj in Plugin.ObjectTable)
{
if (obj is not IPlayerCharacter character)
continue;
if (character.Name.TextValue != payload.PlayerName)
continue;
if (payload.World.Value.IsPublic && character.HomeWorld.RowId != payload.World.RowId)
continue;
return character;
}
return null;
}
internal void Hover(Payload payload) { }
internal unsafe void Click(Chunk chunk, Payload? payload, ImGuiMouseButton button) { }