feat: add channel switcher, settings button, better tooltips

This commit is contained in:
Anna 2022-01-02 16:21:31 -05:00
parent 00421587d0
commit b66ccaa1c0
9 changed files with 244 additions and 44 deletions

View File

@ -1,34 +1,52 @@
namespace ChatTwo.Code;
internal static class InputChannelExt {
internal static ChatType ToChatType(this InputChannel input) {
return input switch {
InputChannel.Tell => ChatType.TellOutgoing,
InputChannel.Say => ChatType.Say,
InputChannel.Party => ChatType.Party,
InputChannel.Alliance => ChatType.Alliance,
InputChannel.Yell => ChatType.Yell,
InputChannel.Shout => ChatType.Shout,
InputChannel.FreeCompany => ChatType.FreeCompany,
InputChannel.PvpTeam => ChatType.PvpTeam,
InputChannel.NoviceNetwork => ChatType.NoviceNetwork,
InputChannel.CrossLinkshell1 => ChatType.CrossLinkshell1,
InputChannel.CrossLinkshell2 => ChatType.CrossLinkshell2,
InputChannel.CrossLinkshell3 => ChatType.CrossLinkshell3,
InputChannel.CrossLinkshell4 => ChatType.CrossLinkshell4,
InputChannel.CrossLinkshell5 => ChatType.CrossLinkshell5,
InputChannel.CrossLinkshell6 => ChatType.CrossLinkshell6,
InputChannel.CrossLinkshell7 => ChatType.CrossLinkshell7,
InputChannel.CrossLinkshell8 => ChatType.CrossLinkshell8,
InputChannel.Linkshell1 => ChatType.Linkshell1,
InputChannel.Linkshell2 => ChatType.Linkshell2,
InputChannel.Linkshell3 => ChatType.Linkshell3,
InputChannel.Linkshell4 => ChatType.Linkshell4,
InputChannel.Linkshell5 => ChatType.Linkshell5,
InputChannel.Linkshell6 => ChatType.Linkshell6,
InputChannel.Linkshell7 => ChatType.Linkshell7,
InputChannel.Linkshell8 => ChatType.Linkshell8,
_ => throw new ArgumentOutOfRangeException(nameof(input), input, null),
};
}
internal static ChatType ToChatType(this InputChannel input) => input switch {
InputChannel.Tell => ChatType.TellOutgoing,
InputChannel.Say => ChatType.Say,
InputChannel.Party => ChatType.Party,
InputChannel.Alliance => ChatType.Alliance,
InputChannel.Yell => ChatType.Yell,
InputChannel.Shout => ChatType.Shout,
InputChannel.FreeCompany => ChatType.FreeCompany,
InputChannel.PvpTeam => ChatType.PvpTeam,
InputChannel.NoviceNetwork => ChatType.NoviceNetwork,
InputChannel.CrossLinkshell1 => ChatType.CrossLinkshell1,
InputChannel.CrossLinkshell2 => ChatType.CrossLinkshell2,
InputChannel.CrossLinkshell3 => ChatType.CrossLinkshell3,
InputChannel.CrossLinkshell4 => ChatType.CrossLinkshell4,
InputChannel.CrossLinkshell5 => ChatType.CrossLinkshell5,
InputChannel.CrossLinkshell6 => ChatType.CrossLinkshell6,
InputChannel.CrossLinkshell7 => ChatType.CrossLinkshell7,
InputChannel.CrossLinkshell8 => ChatType.CrossLinkshell8,
InputChannel.Linkshell1 => ChatType.Linkshell1,
InputChannel.Linkshell2 => ChatType.Linkshell2,
InputChannel.Linkshell3 => ChatType.Linkshell3,
InputChannel.Linkshell4 => ChatType.Linkshell4,
InputChannel.Linkshell5 => ChatType.Linkshell5,
InputChannel.Linkshell6 => ChatType.Linkshell6,
InputChannel.Linkshell7 => ChatType.Linkshell7,
InputChannel.Linkshell8 => ChatType.Linkshell8,
_ => throw new ArgumentOutOfRangeException(nameof(input), input, null),
};
public static uint LinkshellIndex(this InputChannel channel) => channel switch {
InputChannel.Linkshell1 => 0,
InputChannel.Linkshell2 => 1,
InputChannel.Linkshell3 => 2,
InputChannel.Linkshell4 => 3,
InputChannel.Linkshell5 => 4,
InputChannel.Linkshell6 => 5,
InputChannel.Linkshell7 => 6,
InputChannel.Linkshell8 => 7,
InputChannel.CrossLinkshell1 => 0,
InputChannel.CrossLinkshell2 => 1,
InputChannel.CrossLinkshell3 => 2,
InputChannel.CrossLinkshell4 => 3,
InputChannel.CrossLinkshell5 => 4,
InputChannel.CrossLinkshell6 => 5,
InputChannel.CrossLinkshell7 => 6,
InputChannel.CrossLinkshell8 => 7,
_ => uint.MaxValue,
};
}

View File

@ -1,10 +1,15 @@
using ChatTwo.Code;
using System.Runtime.InteropServices;
using System.Text;
using ChatTwo.Code;
using ChatTwo.Util;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace ChatTwo;
@ -13,21 +18,25 @@ internal unsafe class GameFunctions : IDisposable {
private static class Signatures {
internal const string ChatLogRefresh = "40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 49 8B F0 8B FA";
internal const string ChangeChannelName = "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 4D B0 48 8B F8 E8 ?? ?? ?? ?? 41 8B D6";
internal const string ChangeChatChannel = "E8 ?? ?? ?? ?? 0F B7 44 37 ??";
}
private delegate byte ChatLogRefreshDelegate(IntPtr log, ushort eventId, AtkValue* value);
private delegate IntPtr ChangeChannelNameDelegate(IntPtr agent);
private delegate IntPtr ChangeChatChannelDelegate(RaptureShellModule* shell, int channel, uint linkshellIdx, Utf8String* tellTarget, byte one);
internal delegate void ChatActivatedEventDelegate(string? input);
private Plugin Plugin { get; }
private Hook<ChatLogRefreshDelegate>? ChatLogRefreshHook { get; }
private Hook<ChangeChannelNameDelegate>? ChangeChannelNameHook { get; }
private readonly ChangeChatChannelDelegate? _changeChatChannel;
internal event ChatActivatedEventDelegate? ChatActivated;
internal (InputChannel channel, string name) ChatChannel { get; private set; }
internal (InputChannel channel, List<Chunk> name) ChatChannel { get; private set; }
internal GameFunctions(Plugin plugin) {
this.Plugin = plugin;
@ -42,6 +51,10 @@ internal unsafe class GameFunctions : IDisposable {
this.ChangeChannelNameHook.Enable();
}
if (this.Plugin.SigScanner.TryScanText(Signatures.ChangeChatChannel, out var changeChannelPtr)) {
this._changeChatChannel = Marshal.GetDelegateForFunctionPointer<ChangeChatChannelDelegate>(changeChannelPtr);
}
this.Plugin.ClientState.Login += this.Login;
this.Login(null, null);
}
@ -66,6 +79,23 @@ internal unsafe class GameFunctions : IDisposable {
this.ChangeChannelNameDetour((IntPtr) agent);
}
internal void SetChatChannel(InputChannel channel, string? tellTarget = null) {
if (this._changeChatChannel == null) {
return;
}
var bytes = Encoding.UTF8.GetBytes(tellTarget ?? "");
var target = new Utf8String();
fixed (byte* tellTargetPtr = bytes) {
var zero = stackalloc byte[1];
zero[0] = 0;
target.StringPtr = tellTargetPtr == null ? zero : tellTargetPtr;
target.StringLength = bytes.Length;
this._changeChatChannel(RaptureShellModule.Instance, (int) (channel + 1), channel.LinkshellIndex(), &target, 1);
}
}
internal static void SetAddonInteractable(string name, bool interactable) {
var unitManager = AtkStage.GetSingleton()->RaptureAtkUnitManager;
@ -217,7 +247,12 @@ internal unsafe class GameFunctions : IDisposable {
return ret;
}
this.ChatChannel = ((InputChannel) channel, name.TextValue.TrimStart('\uE01E').Trim());
var nameChunks = ChunkUtil.ToChunks(name, null).ToList();
if (nameChunks.Count > 0 && nameChunks[0] is TextChunk text) {
text.Content = text.Content.TrimStart('\uE01E').TrimStart();
}
this.ChatChannel = ((InputChannel) channel, nameChunks);
return ret;
}

View File

@ -7,6 +7,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Logging;
using Dalamud.Utility;
using ImGuiNET;
using ImGuiScene;
namespace ChatTwo;
@ -109,7 +110,20 @@ internal sealed class PayloadHandler {
}
}
private static void InlineIcon(TextureWrap icon) {
var lineHeight = ImGui.CalcTextSize("A").Y;
var cursor = ImGui.GetCursorPos();
ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height));
ImGui.SameLine();
ImGui.SetCursorPos(cursor + new Vector2(icon.Width + 4, (float) icon.Height / 2 - lineHeight / 2));
}
private void HoverStatus(StatusPayload status) {
if (this.Ui.Plugin.TextureCache.GetStatus(status.Status) is { } icon) {
InlineIcon(icon);
}
var name = ChunkUtil.ToChunks(status.Status.Name.ToDalamudString(), null);
this.Log.DrawChunks(name.ToList());
ImGui.Separator();
@ -119,6 +133,10 @@ internal sealed class PayloadHandler {
}
private void HoverItem(ItemPayload item) {
if (this.Ui.Plugin.TextureCache.GetItem(item.Item) is { } icon) {
InlineIcon(icon);
}
var name = ChunkUtil.ToChunks(item.Item.Name.ToDalamudString(), null);
this.Log.DrawChunks(name.ToList());
ImGui.Separator();

View File

@ -46,6 +46,7 @@ public sealed class Plugin : IDalamudPlugin {
internal Configuration Config { get; }
internal XivCommonBase Common { get; }
internal TextureCache TextureCache { get; }
internal GameFunctions Functions { get; }
internal Store Store { get; }
internal PluginUi Ui { get; }
@ -54,6 +55,7 @@ public sealed class Plugin : IDalamudPlugin {
public Plugin() {
this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration();
this.Common = new XivCommonBase();
this.TextureCache = new TextureCache(this.DataManager!);
this.Functions = new GameFunctions(this);
this.Store = new Store(this);
this.Ui = new PluginUi(this);
@ -69,6 +71,7 @@ public sealed class Plugin : IDalamudPlugin {
this.Ui.Dispose();
this.Store.Dispose();
this.Functions.Dispose();
this.TextureCache.Dispose();
this.Common.Dispose();
}

View File

@ -9,6 +9,9 @@ namespace ChatTwo;
internal sealed class PluginUi : IDisposable {
internal Plugin Plugin { get; }
internal bool SettingsVisible;
internal ImFontPtr? RegularFont { get; private set; }
internal ImFontPtr? ItalicFont { get; private set; }
internal Vector4 DefaultText { get; private set; }
@ -55,7 +58,7 @@ internal sealed class PluginUi : IDisposable {
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder());
builder.AddRanges(ImGui.GetIO().Fonts.GetGlyphRangesDefault());
builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─");
builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─");
builder.BuildRanges(out this._ranges);
var regular = this.GetResource("ChatTwo.fonts.NotoSans-Regular.ttf");

59
ChatTwo/TextureCache.cs Executable file
View File

@ -0,0 +1,59 @@
using Dalamud.Data;
using ImGuiScene;
using Lumina.Excel.GeneratedSheets;
namespace ChatTwo;
internal class TextureCache : IDisposable {
private DataManager Data { get; }
private readonly Dictionary<uint, TextureWrap> _itemIcons = new();
private readonly Dictionary<uint, TextureWrap> _statusIcons = new();
internal IReadOnlyDictionary<uint, TextureWrap> ItemIcons => this._itemIcons;
internal IReadOnlyDictionary<uint, TextureWrap> StatusIcons => this._statusIcons;
internal TextureCache(DataManager data) {
this.Data = data;
}
public void Dispose() {
var allIcons = this.ItemIcons.Values
.Concat(this.StatusIcons.Values);
foreach (var tex in allIcons) {
tex.Dispose();
}
}
private void AddIcon(IDictionary<uint, TextureWrap> dict, uint icon) {
if (dict.ContainsKey(icon)) {
return;
}
var tex = this.Data.GetImGuiTextureIcon(icon);
if (tex != null) {
dict[icon] = tex;
}
}
internal void AddItem(Item item) {
this.AddIcon(this._itemIcons, item.Icon);
}
internal void AddStatus(Status status) {
this.AddIcon(this._statusIcons, status.Icon);
}
internal TextureWrap? GetItem(Item item) {
this.AddItem(item);
this.ItemIcons.TryGetValue(item.Icon, out var icon);
return icon;
}
internal TextureWrap? GetStatus(Status status) {
this.AddStatus(status);
this.StatusIcons.TryGetValue(status.Icon, out var icon);
return icon;
}
}

View File

@ -1,12 +1,16 @@
using System.Numerics;
using ChatTwo.Code;
using ChatTwo.Util;
using Dalamud.Interface;
using ImGuiNET;
using ImGuiScene;
using Lumina.Excel.GeneratedSheets;
namespace ChatTwo.Ui;
internal sealed class ChatLog : IUiComponent {
private const string ChatChannelPicker = "chat-channel-picker";
private PluginUi Ui { get; }
internal bool Activate;
@ -143,7 +147,39 @@ internal sealed class ChatLog : IUiComponent {
ImGui.SetKeyboardFocusHere();
}
ImGui.TextUnformatted(this.Ui.Plugin.Functions.ChatChannel.name);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
try {
this.DrawChunks(this.Ui.Plugin.Functions.ChatChannel.name);
} finally {
ImGui.PopStyleVar();
}
var beforeIcon = ImGui.GetCursorPos();
if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment)) {
ImGui.OpenPopup(ChatChannelPicker);
}
if (ImGui.BeginPopup(ChatChannelPicker)) {
foreach (var channel in Enum.GetValues<InputChannel>()) {
var name = this.Ui.Plugin.DataManager.GetExcelSheet<LogFilter>()!
.FirstOrDefault(row => row.LogKind == (byte) channel.ToChatType())
?.Name
?.RawString ?? channel.ToString();
if (ImGui.Selectable(name)) {
this.Ui.Plugin.Functions.SetChatChannel(channel);
}
}
ImGui.EndPopup();
}
ImGui.SameLine();
var afterIcon = ImGui.GetCursorPos();
var buttonWidth = afterIcon.X - beforeIcon.X;
var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth;
var inputType = this.Ui.Plugin.Functions.ChatChannel.channel.ToChatType();
var inputColour = this.Ui.Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol)
@ -154,7 +190,7 @@ internal sealed class ChatLog : IUiComponent {
ImGui.PushStyleColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(inputColour.Value));
}
ImGui.SetNextItemWidth(-1);
ImGui.SetNextItemWidth(inputWidth);
const ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags.EnterReturnsTrue
| ImGuiInputTextFlags.CallbackAlways
| ImGuiInputTextFlags.CallbackHistory;
@ -174,6 +210,12 @@ internal sealed class ChatLog : IUiComponent {
ImGui.PopStyleColor();
}
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.Cog)) {
this.Ui.SettingsVisible ^= true;
}
ImGui.End();
}

View File

@ -9,8 +9,6 @@ namespace ChatTwo.Ui;
internal sealed class Settings : IUiComponent {
private PluginUi Ui { get; }
private bool _visible;
private bool _hideChat;
private bool _nativeItemTooltips;
private float _fontSize;
@ -29,7 +27,7 @@ internal sealed class Settings : IUiComponent {
}
private void Command(string command, string args) {
this._visible ^= true;
this.Ui.SettingsVisible ^= true;
}
private void Initialise() {
@ -42,11 +40,11 @@ internal sealed class Settings : IUiComponent {
}
public void Draw() {
if (!this._visible) {
if (!this.Ui.SettingsVisible) {
return;
}
if (!ImGui.Begin($"{this.Ui.Plugin.Name} settings", ref this._visible)) {
if (!ImGui.Begin($"{this.Ui.Plugin.Name} settings", ref this.Ui.SettingsVisible)) {
ImGui.End();
return;
}
@ -148,13 +146,13 @@ internal sealed class Settings : IUiComponent {
if (ImGui.Button("Save and close")) {
save = true;
this._visible = false;
this.Ui.SettingsVisible = false;
}
ImGui.SameLine();
if (ImGui.Button("Discard")) {
this._visible = false;
this.Ui.SettingsVisible = false;
}
ImGui.End();

View File

@ -1,5 +1,6 @@
using System.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using ImGuiNET;
namespace ChatTwo.Util;
@ -34,14 +35,20 @@ internal static class ImGuiUtil {
PostPayload(payload, handler);
}
if (csText.Length == 0) {
return;
}
foreach (var part in csText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)) {
var bytes = Encoding.UTF8.GetBytes(part);
fixed (byte* rawText = bytes) {
var text = rawText;
var textEnd = text + bytes.Length;
// idk how this is possible, but it is, I guess
// empty string
if (text == null) {
ImGui.TextUnformatted("");
ImGui.TextUnformatted("");
return;
}
@ -63,6 +70,8 @@ internal static class ImGuiUtil {
endPrevLine = ImGuiNative.ImFont_CalcWordWrapPositionA(ImGui.GetFont().NativePtr, scale, text, textEnd, widthLeft);
if (endPrevLine == null) {
ImGui.TextUnformatted("");
ImGui.TextUnformatted("");
break;
}
@ -71,4 +80,19 @@ internal static class ImGuiUtil {
}
}
}
internal static bool IconButton(FontAwesomeIcon icon, string? id = null) {
ImGui.PushFont(UiBuilder.IconFont);
var label = icon.ToIconString();
if (id != null) {
label += $"##{id}";
}
var ret = ImGui.Button(label);
ImGui.PopFont();
return ret;
}
}