Files
HellionChat/HellionChat/GameFunctions/GameFunctions.cs
T
JonKazama-Hellion c4c85cf4b8 docs: unify documentation and streamline code comments
- Translated project documentation (LEARNING-JOURNEY, CONTRIBUTORS, AI_DISCLOSURE) to English for better accessibility.
- Standardized internal code documentation by converting XML-doc blocks to standard comment format.
- Cleaned up inline comments and removed redundant versioning metadata across the codebase.
- Refactored non-functional text elements to improve readability and maintain a consistent style.
2026-05-11 00:52:15 +02:00

270 lines
8.9 KiB
C#
Executable File

using System.Globalization;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Memory;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
namespace HellionChat.GameFunctions;
internal unsafe class GameFunctions : IDisposable
{
internal const string NewGamePlusAddonName = "QuestRedo";
#region Hooks
[Signature(
"E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F",
DetourName = nameof(ResolveTextCommandPlaceholderDetour)
)]
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
private delegate nint ResolveTextCommandPlaceholderDelegate(
nint a1,
byte* placeholderText,
byte a3,
byte a4
);
#endregion
private Plugin Plugin { get; }
internal KeybindManager KeybindManager { get; }
internal Chat Chat { get; }
internal GameFunctions(Plugin plugin)
{
Plugin = plugin;
KeybindManager = new KeybindManager(plugin);
Chat = new Chat(Plugin);
Plugin.GameInteropProvider.InitializeFromAttributes(this);
ResolveTextCommandPlaceholderHook?.Enable();
}
public void Dispose()
{
Chat.Dispose();
KeybindManager.Dispose();
ResolveTextCommandPlaceholderHook?.Dispose();
Marshal.FreeHGlobal(PlaceholderNamePtr);
}
internal void SendFriendRequest(string name, ushort world) =>
ListCommand(name, world, "friendlist");
internal void AddToBlacklist(string name, ushort world) => ListCommand(name, world, "blist");
internal void AddToMuteList(ulong accountId, ulong contentId, string name, short worldId) =>
AgentMutelist.Instance()->Add(accountId, contentId, name, worldId);
internal void AddToTermsList(SeString content) =>
AgentTermFilter.Instance()->OpenNewFilterWindow(content.EncodeWithNullTerminator());
private void ListCommand(string name, ushort world, string commandName)
{
var worldRow = Sheets.WorldSheet.GetRow(world);
ReplacementName = $"{name}@{worldRow.Name.ToString()}";
ChatBox.SendMessage($"/{commandName} add {Placeholder}");
}
private static T* GetAddon<T>(string name)
where T : unmanaged
{
var addon = RaptureAtkModule.Instance()->RaptureAtkUnitManager.GetAddonByName(name);
return addon != null && addon->IsReady ? (T*)addon : null;
}
internal static void SetAddonInteractable(string name, bool interactable)
{
var addon = GetAddon<AtkUnitBase>(name);
if (addon == null)
return;
addon->IsVisible = interactable;
}
internal static void SetChatInteractable(bool interactable)
{
for (var i = 0; i < 4; i++)
SetAddonInteractable($"ChatLogPanel_{i}", interactable);
SetAddonInteractable("ChatLog", interactable);
}
internal static bool IsAddonInteractable(string name)
{
var addon = GetAddon<AtkUnitBase>(name);
return addon != null && addon->IsVisible;
}
internal static void OpenItemTooltip(uint id, ItemKind itemKind)
{
var atkStage = AtkStage.Instance();
var agent = AgentItemDetail.Instance();
var addon = GetAddon<AtkUnitBase>("ItemDetail");
if (agent == null || addon == null)
return;
agent->DetailKind = itemKind == ItemKind.EventItem ? DetailKind.KeyItem : DetailKind.Item;
agent->TypeOrId = id;
agent->Index = 0;
agent->Flag1 &= 0xEF;
agent->ItemId = id;
// TODO: Revert when CS offset lands in a release build.
*(byte*)((nint)agent + 0x21A) = 1;
*(byte*)((nint)agent + 0x21E) = 0;
agent->AddonId = addon->Id;
atkStage->TooltipManager.TooltipType |= 2;
addon->Show(false, 15);
}
internal static void CloseItemTooltip()
{
// Hide addon first to suppress the "addon close" sound.
var addon = GetAddon<AtkUnitBase>("ItemDetail");
if (addon != null)
addon->Hide(true, false, 0);
var agent = AgentItemDetail.Instance();
if (agent != null)
{
var eventData = stackalloc AtkValue[1];
var atkValues = stackalloc AtkValue[1];
atkValues->Type = ValueType.Int;
atkValues->Int = -1;
agent->ReceiveEvent(eventData, atkValues, 1, 1);
}
}
internal static void OpenPartyFinder()
{
// 6.05: 84433A (FF 97 ?? ?? ?? ?? 41 B4 01)
var lfg = AgentLookingForGroup.Instance();
if (lfg->IsAgentActive())
{
var addonId = lfg->GetAddonId();
var atkModule = RaptureAtkModule.Instance();
var atkModuleVtbl = (void**)atkModule->AtkModule.VirtualTable;
var vf27 = (delegate* unmanaged<RaptureAtkModule*, ulong, ulong, byte>)
atkModuleVtbl[27];
vf27(atkModule, addonId, 1);
}
else
{
// 6.05: 8443DD
if (*(uint*)((nint)lfg + 0x2C20) > 0)
lfg->Hide();
else
lfg->Show();
}
}
internal static bool IsMentor() => PlayerState.Instance()->IsMentor();
internal static InfoProxyCommonList.CharacterData[] GetFriends() =>
InfoProxyFriendList.Instance()->CharDataSpan.ToArray();
internal static void OpenQuestLog(RowRef<Quest> quest)
{
var splits = quest.Value.Id.ToString().Split("_");
if (splits.Length != 2)
{
Plugin.ChatGui.Print("QuestId is wrongly formatted");
return;
}
if (
!uint.TryParse(
splits[1],
NumberStyles.Any,
CultureInfo.InvariantCulture,
out var questId
)
)
{
Plugin.ChatGui.Print("Unable to parse quest id");
return;
}
AgentQuestJournal.Instance()->OpenForQuest(questId, 1);
}
internal static void OpenPartyFinder(uint id) =>
AgentLookingForGroup.Instance()->OpenListing(id);
internal static void OpenAchievement(uint id) => AgentAchievement.Instance()->OpenById(id);
internal static bool IsInInstance() => Plugin.Condition[ConditionFlag.BoundByDuty56];
internal static bool TryOpenAdventurerPlate(ulong playerId)
{
try
{
AgentCharaCard.Instance()->OpenCharaCard(playerId);
return true;
}
catch (Exception e)
{
Plugin.Log.Warning(e, "Unable to open adventurer plate");
return false;
}
}
internal static void ClickNoviceNetworkButton()
{
var agent = AgentChatLog.Instance();
var value = new AtkValue { Type = ValueType.Int, Int = 3 }; // case 3
var result = 0;
var vf0 = *(delegate* unmanaged<AgentChatLog*, int*, AtkValue*, ulong, ulong, int*>*)
agent->VirtualTable;
vf0(agent, &result, &value, 0, 0);
}
private const int PlaceholderBufferSize = 128;
private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(PlaceholderBufferSize);
private readonly string Placeholder = $"<{Guid.NewGuid():N}>";
private string? ReplacementName;
private nint ResolveTextCommandPlaceholderDetour(
nint a1,
byte* placeholderText,
byte a3,
byte a4
)
{
// Hook field is nullable due to the Signature attribute, but will never
// be null during normal execution; guard covers the teardown race only.
if (ResolveTextCommandPlaceholderHook is null)
return nint.Zero;
var placeholder = MemoryHelper.ReadStringNullTerminated((nint)placeholderText);
if (ReplacementName == null || placeholder != Placeholder)
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
// Guard against a malformed ReplacementName overflowing the 128-byte buffer.
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
if (byteCount >= PlaceholderBufferSize)
{
Plugin.Log.Warning(
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
);
ReplacementName = null;
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
}
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
ReplacementName = null;
return PlaceholderNamePtr;
}
}