From 186b8ed62ca16c3b5362458f0e8d745addb9e0ad Mon Sep 17 00:00:00 2001 From: Anna Clemens Date: Sat, 27 Feb 2021 14:23:52 -0500 Subject: [PATCH 1/8] feat: add Party Finder listing event The event is read-only except for the ability to hide listings. --- Dalamud/Game/Internal/Gui/GameGui.cs | 4 + .../Gui/PartyFinderAddressResolver.cs | 11 + Dalamud/Game/Internal/Gui/PartyFinderGui.cs | 117 +++++ .../Game/Internal/Gui/Structs/PartyFinder.cs | 467 ++++++++++++++++++ 4 files changed, 599 insertions(+) create mode 100755 Dalamud/Game/Internal/Gui/PartyFinderAddressResolver.cs create mode 100755 Dalamud/Game/Internal/Gui/PartyFinderGui.cs create mode 100755 Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs diff --git a/Dalamud/Game/Internal/Gui/GameGui.cs b/Dalamud/Game/Internal/Gui/GameGui.cs index b15effe7d..0175b0791 100644 --- a/Dalamud/Game/Internal/Gui/GameGui.cs +++ b/Dalamud/Game/Internal/Gui/GameGui.cs @@ -10,6 +10,7 @@ namespace Dalamud.Game.Internal.Gui { private GameGuiAddressResolver Address { get; } public ChatGui Chat { get; private set; } + public PartyFinderGui PartyFinder { get; private set; } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr SetGlobalBgmDelegate(UInt16 bgmKey, byte a2, UInt32 a3, UInt32 a4, UInt32 a5, byte a6); @@ -106,6 +107,7 @@ namespace Dalamud.Game.Internal.Gui { Log.Verbose("GetUIObject address {Address}", Address.GetUIObject); Chat = new ChatGui(Address.ChatManager, scanner, dalamud); + PartyFinder = new PartyFinderGui(scanner, dalamud); this.setGlobalBgmHook = new Hook(Address.SetGlobalBgm, @@ -415,6 +417,7 @@ namespace Dalamud.Game.Internal.Gui { public void Enable() { Chat.Enable(); + PartyFinder.Enable(); this.setGlobalBgmHook.Enable(); this.handleItemHoverHook.Enable(); this.handleItemOutHook.Enable(); @@ -425,6 +428,7 @@ namespace Dalamud.Game.Internal.Gui { public void Dispose() { Chat.Dispose(); + PartyFinder.Dispose(); this.setGlobalBgmHook.Dispose(); this.handleItemHoverHook.Dispose(); this.handleItemOutHook.Dispose(); diff --git a/Dalamud/Game/Internal/Gui/PartyFinderAddressResolver.cs b/Dalamud/Game/Internal/Gui/PartyFinderAddressResolver.cs new file mode 100755 index 000000000..03e27b2a3 --- /dev/null +++ b/Dalamud/Game/Internal/Gui/PartyFinderAddressResolver.cs @@ -0,0 +1,11 @@ +using System; + +namespace Dalamud.Game.Internal.Gui { + class PartyFinderAddressResolver : BaseAddressResolver { + public IntPtr ReceiveListing { get; private set; } + + protected override void Setup64Bit(SigScanner sig) { + ReceiveListing = sig.ScanText("40 53 41 57 48 83 EC 28 48 8B D9"); + } + } +} diff --git a/Dalamud/Game/Internal/Gui/PartyFinderGui.cs b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs new file mode 100755 index 000000000..678251212 --- /dev/null +++ b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs @@ -0,0 +1,117 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Game.Internal.Gui.Structs; +using Dalamud.Hooking; +using Serilog; + +namespace Dalamud.Game.Internal.Gui { + public sealed class PartyFinderGui : IDisposable { + #region Events + + public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args); + + /// + /// Event fired each time the game receives an individual Party Finder listing. Cannot modify listings but can + /// hide them. + /// + public event PartyFinderListingEventDelegate ReceiveListing; + + #endregion + + #region Hooks + + private readonly Hook receiveListingHook; + + #endregion + + #region Delegates + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data); + + #endregion + + private Dalamud Dalamud { get; } + private PartyFinderAddressResolver Address { get; } + private IntPtr Memory { get; } + + public PartyFinderGui(SigScanner scanner, Dalamud dalamud) { + Dalamud = dalamud; + + Address = new PartyFinderAddressResolver(); + Address.Setup(scanner); + + Memory = Marshal.AllocHGlobal(PartyFinder.PacketInfo.PacketSize); + + this.receiveListingHook = new Hook(Address.ReceiveListing, new ReceiveListingDelegate(HandleReceiveListingDetour)); + } + + public void Enable() { + this.receiveListingHook.Enable(); + } + + public void Dispose() { + this.receiveListingHook.Dispose(); + Marshal.FreeHGlobal(Memory); + } + + private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data) { + try { + HandleListingEvents(data); + } catch (Exception ex) { + Log.Error(ex, "Exception on ReceiveListing hook."); + } + + this.receiveListingHook.Original(managerPtr, data); + } + + private void HandleListingEvents(IntPtr data) { + var dataPtr = data + 0x10; + + var packet = Marshal.PtrToStructure(dataPtr); + + // rewriting is an expensive operation, so only do it if necessary + var needToRewrite = false; + + for (var i = 0; i < packet.listings.Length; i++) { + // these are empty slots that are not shown to the player + if (packet.listings[i].IsNull()) { + continue; + } + + var listing = new PartyFinderListing(packet.listings[i], Dalamud.Data); + var args = new PartyFinderListingEventArgs(); + ReceiveListing?.Invoke(listing, args); + + if (args.Visible) { + continue; + } + + // hide the listing from the player by setting it to a null listing + packet.listings[i] = new PartyFinder.Listing(); + needToRewrite = true; + } + + if (!needToRewrite) { + return; + } + + // write our struct into the memory (doing this directly crashes the game) + Marshal.StructureToPtr(packet, Memory, false); + + // copy our new memory over the game's + unsafe { + Buffer.MemoryCopy( + (void*) Memory, + (void*) dataPtr, + PartyFinder.PacketInfo.PacketSize, + PartyFinder.PacketInfo.PacketSize + ); + } + } + } + + public class PartyFinderListingEventArgs { + public bool Visible { get; set; } = true; + } +} diff --git a/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs b/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs new file mode 100755 index 000000000..5688ca021 --- /dev/null +++ b/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs @@ -0,0 +1,467 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; + +namespace Dalamud.Game.Internal.Gui.Structs { + #region Raw structs + + internal static class PartyFinder { + public static class PacketInfo { + public static readonly int PacketSize = Marshal.SizeOf(); + } + + [StructLayout(LayoutKind.Sequential)] + public readonly struct Packet { + private readonly int unk0; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + private readonly byte[] padding1; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public readonly Listing[] listings; + } + + [StructLayout(LayoutKind.Sequential)] + public readonly struct Listing { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + private readonly byte[] header1; + + internal readonly uint id; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + private readonly byte[] header2; + + private readonly uint unknownInt1; + private readonly ushort unknownShort1; + private readonly ushort unknownShort2; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] + private readonly byte[] header3; + + internal readonly byte category; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + private readonly byte[] header4; + + internal readonly ushort duty; + internal readonly byte dutyType; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)] + private readonly byte[] header5; + + internal readonly ushort world; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + private readonly byte[] header6; + + internal readonly byte objective; + internal readonly byte beginnersWelcome; + internal readonly byte conditions; + internal readonly byte dutyFinderSettings; + internal readonly byte lootRules; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + private readonly byte[] header7; // all zero in every pf I've examined + + private readonly uint lastPatchHotfixTimestamp; // last time the servers were restarted? + internal readonly ushort secondsRemaining; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + private readonly byte[] header8; // 00 00 01 00 00 00 in every pf I've examined + + internal readonly ushort minimumItemLevel; + internal readonly ushort homeWorld; + internal readonly ushort currentWorld; + + private readonly byte header9; + + internal readonly byte numSlots; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + private readonly byte[] header10; + + internal readonly byte searchArea; + + private readonly byte header11; + + internal readonly byte numParties; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + private readonly byte[] header12; // 00 00 00 always. maybe numParties is a u32? + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + internal readonly uint[] slots; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + internal readonly byte[] jobsPresent; + + // Note that ByValTStr will not work here because the strings are UTF-8 and there's only a CharSet for UTF-16 in C#. + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + private readonly byte[] name; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 192)] + private readonly byte[] description; + + // 128 (0x80) before name and desc + // 160 (0xA0) with name (32 bytes/0x20) + // 352 (0x160) with both (192 bytes/0xC0) + + private static string HandleString(IEnumerable bytes) { + var nonNull = bytes.TakeWhile(b => b != 0).ToArray(); + return Encoding.UTF8.GetString(nonNull); + } + + internal string Name() { + return HandleString(this.name); + } + + internal string Description() { + return HandleString(this.description); + } + + internal bool IsNull() { + // a valid party finder must have at least one slot set + return this.slots.All(slot => slot == 0); + } + } + } + + #endregion + + #region Read-only classes + + public class PartyFinderListing { + /// + /// The ID assigned to this listing by the game's server. + /// + public uint Id { get; } + /// + /// The name of the player hosting this listing. + /// + public string Name { get; } + /// + /// The description of this listing as set by the host. May be multiple lines. + /// + public string Description { get; } + /// + /// The world that this listing was created on. + /// + public Lazy World { get; } + /// + /// The home world of the listing's host. + /// + public Lazy HomeWorld { get; } + /// + /// The current world of the listing's host. + /// + public Lazy CurrentWorld { get; } + /// + /// The Party Finder category this listing is listed under. + /// + public Category Category { get; } + /// + /// The row ID of the duty this listing is for. May be 0 for non-duty listings. + /// + public ushort RawDuty { get; } + /// + /// The duty this listing is for. May be null for non-duty listings. + /// + public Lazy Duty { get; } + /// + /// The type of duty this listing is for. + /// + public DutyType DutyType { get; } + /// + /// If this listing is beginner-friendly. Shown with a sprout icon in-game. + /// + public bool BeginnersWelcome { get; } + /// + /// How many seconds this listing will continue to be available for. It may end before this time if the party + /// fills or the host ends it early. + /// + public ushort SecondsRemaining { get; } + /// + /// The minimum item level required to join this listing. + /// + public ushort MinimumItemLevel { get; } + /// + /// The number of parties this listing is recruiting for. + /// + public byte Parties { get; } + /// + /// The number of player slots this listing is recruiting for. + /// + public byte SlotsAvailable { get; } + + /// + /// A list of player slots that the Party Finder is accepting. + /// + public IReadOnlyCollection Slots => this.slots; + + /// + /// The objective of this listing. + /// + public ObjectiveFlags Objective => (ObjectiveFlags) this.objective; + + /// + /// The conditions of this listing. + /// + public ConditionFlags Conditions => (ConditionFlags) this.conditions; + + /// + /// The Duty Finder settings that will be used for this listing. + /// + public DutyFinderSettingsFlags DutyFinderSettings => (DutyFinderSettingsFlags) this.dutyFinderSettings; + + /// + /// The loot rules that will be used for this listing. + /// + public LootRuleFlags LootRules => (LootRuleFlags) this.lootRules; + + /// + /// Where this listing is searching. Note that this is also used for denoting alliance raid listings and one + /// player per job. + /// + public SearchAreaFlags SearchArea => (SearchAreaFlags) this.searchArea; + + /// + /// A list of the class/job IDs that are currently present in the party. + /// + public IReadOnlyCollection RawJobsPresent => this.jobsPresent; + /// + /// A list of the classes/jobs that are currently present in the party. + /// + public IReadOnlyCollection> JobsPresent { get; } + + #region Backing fields + + private readonly byte objective; + private readonly byte conditions; + private readonly byte dutyFinderSettings; + private readonly byte lootRules; + private readonly byte searchArea; + private readonly PartyFinderSlot[] slots; + private readonly byte[] jobsPresent; + + #endregion + + #region Indexers + + public bool this[ObjectiveFlags flag] => this.objective == 0 || (this.objective & (uint) flag) > 0; + + public bool this[ConditionFlags flag] => this.conditions == 0 || (this.conditions & (uint) flag) > 0; + + public bool this[DutyFinderSettingsFlags flag] => this.dutyFinderSettings == 0 || (this.dutyFinderSettings & (uint) flag) > 0; + + public bool this[LootRuleFlags flag] => this.lootRules == 0 || (this.lootRules & (uint) flag) > 0; + + public bool this[SearchAreaFlags flag] => this.searchArea == 0 || (this.searchArea & (uint) flag) > 0; + + #endregion + + internal PartyFinderListing(PartyFinder.Listing listing, DataManager dataManager) { + this.objective = listing.objective; + this.conditions = listing.conditions; + this.dutyFinderSettings = listing.dutyFinderSettings; + this.lootRules = listing.lootRules; + this.searchArea = listing.searchArea; + this.slots = listing.slots.Select(accepting => new PartyFinderSlot(accepting)).ToArray(); + this.jobsPresent = listing.jobsPresent; + + Id = listing.id; + Name = listing.Name(); + Description = listing.Description(); + World = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.world)); + HomeWorld = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.homeWorld)); + CurrentWorld = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.currentWorld)); + Category = (Category) listing.category; + RawDuty = listing.duty; + Duty = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.duty)); + DutyType = (DutyType) listing.dutyType; + BeginnersWelcome = listing.beginnersWelcome == 1; + SecondsRemaining = listing.secondsRemaining; + MinimumItemLevel = listing.minimumItemLevel; + Parties = listing.numParties; + SlotsAvailable = listing.numSlots; + JobsPresent = listing.jobsPresent + .Select(id => new Lazy(() => id == 0 + ? null + : dataManager.GetExcelSheet().GetRow(id))) + .ToArray(); + } + } + + /// + /// A player slot in a Party Finder listing. + /// + public class PartyFinderSlot { + private readonly uint accepting; + private JobFlags[] listAccepting; + + /// + /// List of jobs that this slot is accepting. + /// + public IReadOnlyCollection Accepting { + get { + if (this.listAccepting != null) { + return this.listAccepting; + } + + this.listAccepting = Enum.GetValues(typeof(JobFlags)) + .Cast() + .Where(flag => this[flag]) + .ToArray(); + + return this.listAccepting; + } + } + + /// + /// Tests if this slot is accepting a job. + /// + /// Job to test + public bool this[JobFlags flag] => (this.accepting & (uint) flag) > 0; + + internal PartyFinderSlot(uint accepting) { + this.accepting = accepting; + } + } + + [Flags] + public enum SearchAreaFlags : uint { + DataCentre = 1 << 0, + Private = 1 << 1, + AllianceRaid = 1 << 2, + World = 1 << 3, + OnePlayerPerJob = 1 << 5, + } + + [Flags] + public enum JobFlags { + Gladiator = 1 << 1, + Pugilist = 1 << 2, + Marauder = 1 << 3, + Lancer = 1 << 4, + Archer = 1 << 5, + Conjurer = 1 << 6, + Thaumaturge = 1 << 7, + Paladin = 1 << 8, + Monk = 1 << 9, + Warrior = 1 << 10, + Dragoon = 1 << 11, + Bard = 1 << 12, + WhiteMage = 1 << 13, + BlackMage = 1 << 14, + Arcanist = 1 << 15, + Summoner = 1 << 16, + Scholar = 1 << 17, + Rogue = 1 << 18, + Ninja = 1 << 19, + Machinist = 1 << 20, + DarkKnight = 1 << 21, + Astrologian = 1 << 22, + Samurai = 1 << 23, + RedMage = 1 << 24, + BlueMage = 1 << 25, + Gunbreaker = 1 << 26, + Dancer = 1 << 27, + } + + public static class JobFlagsExt { + /// + /// Get the actual ClassJob from the in-game sheets for this JobFlags. + /// + /// A JobFlags enum member + /// A DataManager to get the ClassJob from + /// A ClassJob if found or null if not + public static ClassJob ClassJob(this JobFlags job, DataManager data) { + var jobs = data.GetExcelSheet(); + + uint? row = job switch { + JobFlags.Gladiator => 1, + JobFlags.Pugilist => 2, + JobFlags.Marauder => 3, + JobFlags.Lancer => 4, + JobFlags.Archer => 5, + JobFlags.Conjurer => 6, + JobFlags.Thaumaturge => 7, + JobFlags.Paladin => 19, + JobFlags.Monk => 20, + JobFlags.Warrior => 21, + JobFlags.Dragoon => 22, + JobFlags.Bard => 23, + JobFlags.WhiteMage => 24, + JobFlags.BlackMage => 25, + JobFlags.Arcanist => 26, + JobFlags.Summoner => 27, + JobFlags.Scholar => 28, + JobFlags.Rogue => 29, + JobFlags.Ninja => 30, + JobFlags.Machinist => 31, + JobFlags.DarkKnight => 32, + JobFlags.Astrologian => 33, + JobFlags.Samurai => 34, + JobFlags.RedMage => 35, + JobFlags.BlueMage => 36, + JobFlags.Gunbreaker => 37, + JobFlags.Dancer => 38, + _ => null, + }; + + return row == null ? null : jobs.GetRow((uint) row); + } + } + + [Flags] + public enum ObjectiveFlags : uint { + None = 0, + DutyCompletion = 1, + Practice = 2, + Loot = 4, + } + + [Flags] + public enum ConditionFlags : uint { + None = 1, + DutyComplete = 2, + DutyIncomplete = 4, + } + + [Flags] + public enum DutyFinderSettingsFlags : uint { + None = 0, + UndersizedParty = 1 << 0, + MinimumItemLevel = 1 << 1, + SilenceEcho = 1 << 2, + } + + [Flags] + public enum LootRuleFlags : uint { + None = 0, + GreedOnly = 1, + Lootmaster = 2, + } + + public enum Category { + Duty = 0, + QuestBattles = 1 << 0, + Fates = 1 << 1, + TreasureHunt = 1 << 2, + TheHunt = 1 << 3, + GatheringForays = 1 << 4, + DeepDungeons = 1 << 5, + AdventuringForays = 1 << 6, + } + + public enum DutyType { + Other = 0, + Roulette = 1 << 0, + Normal = 1 << 1, + } + + #endregion +} From 7f51a3e5dfe74fe6b7056b445fa2b2218078ada7 Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Wed, 3 Mar 2021 11:32:21 +0100 Subject: [PATCH 2/8] fix: slow Any method --- Dalamud/Game/ClientState/Condition.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Dalamud/Game/ClientState/Condition.cs b/Dalamud/Game/ClientState/Condition.cs index a1fc9226c..9b1e42c29 100644 --- a/Dalamud/Game/ClientState/Condition.cs +++ b/Dalamud/Game/ClientState/Condition.cs @@ -40,22 +40,16 @@ namespace Dalamud.Game.ClientState } public bool Any() { - var didAny = false; - for (var i = 0; i < MaxConditionEntries; i++) { var typedCondition = (ConditionFlag)i; var cond = this[typedCondition]; - if (!cond) - { - continue; - } - - didAny = true; + if (cond) + return true; } - return didAny; + return false; } } } From e69a76c40187468687f6cd0682615d416d0a1afe Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Wed, 3 Mar 2021 01:11:35 +0100 Subject: [PATCH 3/8] fix: remove ambiguity with raw plugins in installer --- Dalamud/Plugin/PluginInstallerWindow.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Plugin/PluginInstallerWindow.cs b/Dalamud/Plugin/PluginInstallerWindow.cs index 7361264f5..523033afb 100644 --- a/Dalamud/Plugin/PluginInstallerWindow.cs +++ b/Dalamud/Plugin/PluginInstallerWindow.cs @@ -460,7 +460,9 @@ namespace Dalamud.Plugin if (installedPlugin.IsRaw) { ImGui.SameLine(); ImGui.TextColored(new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - " To update or disable this plugin, please remove it from the devPlugins folder."); + this.dalamud.PluginRepository.PluginMaster.Any(x => x.InternalName == installedPlugin.Definition.InternalName) + ? " This plugin is available in one of your repos, please remove it from the devPlugins folder." + : " To disable this plugin, please remove it from the devPlugins folder."); } } From 66b4d19603cb99ffaece286872374ea2f6d62db1 Mon Sep 17 00:00:00 2001 From: Anna Clemens Date: Wed, 3 Mar 2021 21:08:45 -0500 Subject: [PATCH 4/8] fix: use SeString for PF text --- Dalamud/Game/Internal/Gui/PartyFinderGui.cs | 2 +- .../Game/Internal/Gui/Structs/PartyFinder.cs | 33 +++++-------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/Dalamud/Game/Internal/Gui/PartyFinderGui.cs b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs index 678251212..20a66825e 100755 --- a/Dalamud/Game/Internal/Gui/PartyFinderGui.cs +++ b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs @@ -79,7 +79,7 @@ namespace Dalamud.Game.Internal.Gui { continue; } - var listing = new PartyFinderListing(packet.listings[i], Dalamud.Data); + var listing = new PartyFinderListing(packet.listings[i], Dalamud.Data, Dalamud.SeStringManager); var args = new PartyFinderListingEventArgs(); ReceiveListing?.Invoke(listing, args); diff --git a/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs b/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs index 5688ca021..cd8f133e6 100755 --- a/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs +++ b/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; -using System.Text; using Dalamud.Data; +using Dalamud.Game.Chat.SeStringHandling; using Lumina.Excel.GeneratedSheets; namespace Dalamud.Game.Internal.Gui.Structs { @@ -101,27 +101,10 @@ namespace Dalamud.Game.Internal.Gui.Structs { // Note that ByValTStr will not work here because the strings are UTF-8 and there's only a CharSet for UTF-16 in C#. [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] - private readonly byte[] name; + internal readonly byte[] name; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 192)] - private readonly byte[] description; - - // 128 (0x80) before name and desc - // 160 (0xA0) with name (32 bytes/0x20) - // 352 (0x160) with both (192 bytes/0xC0) - - private static string HandleString(IEnumerable bytes) { - var nonNull = bytes.TakeWhile(b => b != 0).ToArray(); - return Encoding.UTF8.GetString(nonNull); - } - - internal string Name() { - return HandleString(this.name); - } - - internal string Description() { - return HandleString(this.description); - } + internal readonly byte[] description; internal bool IsNull() { // a valid party finder must have at least one slot set @@ -142,11 +125,11 @@ namespace Dalamud.Game.Internal.Gui.Structs { /// /// The name of the player hosting this listing. /// - public string Name { get; } + public SeString Name { get; } /// /// The description of this listing as set by the host. May be multiple lines. /// - public string Description { get; } + public SeString Description { get; } /// /// The world that this listing was created on. /// @@ -263,7 +246,7 @@ namespace Dalamud.Game.Internal.Gui.Structs { #endregion - internal PartyFinderListing(PartyFinder.Listing listing, DataManager dataManager) { + internal PartyFinderListing(PartyFinder.Listing listing, DataManager dataManager, SeStringManager seStringManager) { this.objective = listing.objective; this.conditions = listing.conditions; this.dutyFinderSettings = listing.dutyFinderSettings; @@ -273,8 +256,8 @@ namespace Dalamud.Game.Internal.Gui.Structs { this.jobsPresent = listing.jobsPresent; Id = listing.id; - Name = listing.Name(); - Description = listing.Description(); + Name = seStringManager.Parse(listing.name); + Description = seStringManager.Parse(listing.description); World = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.world)); HomeWorld = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.homeWorld)); CurrentWorld = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.currentWorld)); From 5dde360e0240569fd2c91ce0531bfb260896be00 Mon Sep 17 00:00:00 2001 From: Anna Clemens Date: Wed, 3 Mar 2021 21:10:46 -0500 Subject: [PATCH 5/8] fix: only look at non-null bytes --- Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs b/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs index cd8f133e6..91de07913 100755 --- a/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs +++ b/Dalamud/Game/Internal/Gui/Structs/PartyFinder.cs @@ -256,8 +256,8 @@ namespace Dalamud.Game.Internal.Gui.Structs { this.jobsPresent = listing.jobsPresent; Id = listing.id; - Name = seStringManager.Parse(listing.name); - Description = seStringManager.Parse(listing.description); + Name = seStringManager.Parse(listing.name.TakeWhile(b => b != 0).ToArray()); + Description = seStringManager.Parse(listing.description.TakeWhile(b => b != 0).ToArray()); World = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.world)); HomeWorld = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.homeWorld)); CurrentWorld = new Lazy(() => dataManager.GetExcelSheet().GetRow(listing.currentWorld)); From 3732ac332f61da69b0214ecefb106df8626d575f Mon Sep 17 00:00:00 2001 From: Cara Date: Fri, 12 Mar 2021 16:28:36 +1030 Subject: [PATCH 6/8] Fix copy buttons in data window not working due to conflicting Ids ImGui really sucks at this --- Dalamud/Interface/DalamudDataWindow.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/DalamudDataWindow.cs b/Dalamud/Interface/DalamudDataWindow.cs index 3899697bc..cc4b0ad8b 100644 --- a/Dalamud/Interface/DalamudDataWindow.cs +++ b/Dalamud/Interface/DalamudDataWindow.cs @@ -42,6 +42,8 @@ namespace Dalamud.Interface private UIDebug UIDebug = null; + private uint copyButtonIndex = 0; + public DalamudDataWindow(Dalamud dalamud) { this.dalamud = dalamud; @@ -57,6 +59,7 @@ namespace Dalamud.Interface } public bool Draw() { + this.copyButtonIndex = 0; ImGui.SetNextWindowSize(new Vector2(500, 500), ImGuiCond.FirstUseEver); var isOpen = true; @@ -101,7 +104,7 @@ namespace Dalamud.Interface } ImGui.Text($"Result: {this.sigResult.ToInt64():X}"); ImGui.SameLine(); - if (ImGui.Button("C")) { + if (ImGui.Button($"C{this.copyButtonIndex++}")) { ImGui.SetClipboardText(this.sigResult.ToInt64().ToString("x")); } @@ -112,7 +115,7 @@ namespace Dalamud.Interface $" {valueTuple.Item1} - 0x{valueTuple.Item2.ToInt64():x}"); ImGui.SameLine(); - if (ImGui.Button("C")) { + if (ImGui.Button($"C##copyAddress{copyButtonIndex++}")) { ImGui.SetClipboardText(valueTuple.Item2.ToInt64().ToString("x")); } } @@ -427,7 +430,7 @@ namespace Dalamud.Interface ImGui.TextUnformatted(actorString); ImGui.SameLine(); - if (ImGui.Button("C")) { + if (ImGui.Button($"C##{this.copyButtonIndex++}")) { ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X")); } From 6ac126a743dbd650537253312553e45d5263ad52 Mon Sep 17 00:00:00 2001 From: kalilistic <35899782+kalilistic@users.noreply.github.com> Date: Sun, 28 Mar 2021 13:59:19 -0400 Subject: [PATCH 7/8] add current ui lang and change event --- Dalamud/Interface/DalamudInterface.cs | 2 +- Dalamud/Localization.cs | 21 ++++++++++++++------ Dalamud/Plugin/DalamudPluginInterface.cs | 25 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/DalamudInterface.cs b/Dalamud/Interface/DalamudInterface.cs index 5319ba9e5..560d5980b 100644 --- a/Dalamud/Interface/DalamudInterface.cs +++ b/Dalamud/Interface/DalamudInterface.cs @@ -223,7 +223,7 @@ namespace Dalamud.Interface { if (ImGui.MenuItem("From Fallbacks")) { - Loc.SetupWithFallbacks(); + this.dalamud.LocalizationManager.SetupWithFallbacks(); } if (ImGui.MenuItem("From UICulture")) diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index 838b45e13..b109db62d 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -13,8 +13,11 @@ namespace Dalamud class Localization { private readonly string workingDirectory; + private const string FallbackLangCode = "en"; public static readonly string[] ApplicableLangCodes = { "de", "ja", "fr", "it", "es", "ko", "no", "ru" }; - + public delegate void LocalizationChangedDelegate(string langCode); + public event LocalizationChangedDelegate OnLocalizationChanged; + public Localization(string workingDirectory) { this.workingDirectory = workingDirectory; } @@ -28,22 +31,28 @@ namespace Dalamud if (ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x)) { SetupWithLangCode(currentUiLang.TwoLetterISOLanguageName); } else { - Loc.SetupWithFallbacks(); + SetupWithFallbacks(); } } catch (Exception ex) { Log.Error(ex, "Could not get language information. Setting up fallbacks."); - Loc.SetupWithFallbacks(); + SetupWithFallbacks(); } } + public void SetupWithFallbacks() { + OnLocalizationChanged?.Invoke(FallbackLangCode); + Loc.SetupWithFallbacks(); + } + public void SetupWithLangCode(string langCode) { - if (langCode.ToLower() == "en") { - Loc.SetupWithFallbacks(); + if (langCode.ToLower() == FallbackLangCode) { + SetupWithFallbacks(); return; } - + + OnLocalizationChanged?.Invoke(langCode); Loc.Setup(File.ReadAllText(Path.Combine(this.workingDirectory, "UIRes", "loc", "dalamud", $"dalamud_{langCode}.json"))); } } diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index e6da93282..de4832cc3 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -87,7 +87,23 @@ namespace Dalamud.Plugin #else public bool IsDebugging => this.dalamud.DalamudUi.IsDevMenu; #endif + + /// + /// Event that gets fired when loc is changed + /// + public event LanguageChangedDelegate OnLanguageChanged; + + /// + /// Delegate for localization change with two-letter iso lang code + /// + /// + public delegate void LanguageChangedDelegate(string langCode); + /// + /// Current ui language in two-letter iso format + /// + public string UiLanguage { get; private set; } + private readonly Dalamud dalamud; private readonly string pluginName; private readonly PluginConfigurations configs; @@ -109,6 +125,14 @@ namespace Dalamud.Plugin this.dalamud = dalamud; this.pluginName = pluginName; this.configs = configs; + + this.UiLanguage = this.dalamud.Configuration.LanguageOverride; + dalamud.LocalizationManager.OnLocalizationChanged += OnLocalizationChanged; + } + + private void OnLocalizationChanged(string langCode) { + this.UiLanguage = langCode; + OnLanguageChanged?.Invoke(langCode); } /// @@ -117,6 +141,7 @@ namespace Dalamud.Plugin public void Dispose() { this.UiBuilder.Dispose(); this.Framework.Gui.Chat.RemoveChatLinkHandler(this.pluginName); + this.dalamud.LocalizationManager.OnLocalizationChanged -= OnLocalizationChanged; } /// From 8e31f260e1600a6f3ae25c7e1747fe3dbef00298 Mon Sep 17 00:00:00 2001 From: kalilistic <35899782+kalilistic@users.noreply.github.com> Date: Sun, 28 Mar 2021 17:47:15 -0400 Subject: [PATCH 8/8] add open log at startup and persistent log settings --- Dalamud/Configuration/DalamudConfiguration.cs | 2 ++ Dalamud/Interface/DalamudInterface.cs | 7 +++++-- Dalamud/Interface/DalamudLogWindow.cs | 18 +++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Dalamud/Configuration/DalamudConfiguration.cs b/Dalamud/Configuration/DalamudConfiguration.cs index 77d22c730..1ee8af5ad 100644 --- a/Dalamud/Configuration/DalamudConfiguration.cs +++ b/Dalamud/Configuration/DalamudConfiguration.cs @@ -34,6 +34,8 @@ namespace Dalamud public bool PrintPluginsWelcomeMsg { get; set; } = true; public bool AutoUpdatePlugins { get; set; } = false; + public bool LogAutoScroll { get; set; } = true; + public bool LogOpenAtStartup { get; set; } [JsonIgnore] public string ConfigPath; diff --git a/Dalamud/Interface/DalamudInterface.cs b/Dalamud/Interface/DalamudInterface.cs index 5319ba9e5..abf5c1127 100644 --- a/Dalamud/Interface/DalamudInterface.cs +++ b/Dalamud/Interface/DalamudInterface.cs @@ -26,6 +26,9 @@ namespace Dalamud.Interface public DalamudInterface(Dalamud dalamud) { this.dalamud = dalamud; + if (dalamud.Configuration.LogOpenAtStartup) { + OpenLog(); + } } private bool isImguiDrawDemoWindow = false; @@ -97,7 +100,7 @@ namespace Dalamud.Interface ImGui.Separator(); if (ImGui.MenuItem("Open Log window")) { - this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager); + this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager, this.dalamud.Configuration); this.isImguiDrawLogWindow = true; } if (ImGui.BeginMenu("Set log level...")) @@ -336,7 +339,7 @@ namespace Dalamud.Interface } public void OpenLog() { - this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager); + this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager, this.dalamud.Configuration); this.isImguiDrawLogWindow = true; } diff --git a/Dalamud/Interface/DalamudLogWindow.cs b/Dalamud/Interface/DalamudLogWindow.cs index 5e96c8213..9da1269be 100644 --- a/Dalamud/Interface/DalamudLogWindow.cs +++ b/Dalamud/Interface/DalamudLogWindow.cs @@ -13,15 +13,20 @@ namespace Dalamud.Interface { class DalamudLogWindow : IDisposable { private readonly CommandManager commandManager; - private bool autoScroll = true; + private readonly DalamudConfiguration configuration; + private bool autoScroll; + private bool openAtStartup; private readonly List<(string line, Vector4 color)> logText = new List<(string line, Vector4 color)>(); private readonly object renderLock = new object(); private string commandText = string.Empty; - public DalamudLogWindow(CommandManager commandManager) { + public DalamudLogWindow(CommandManager commandManager, DalamudConfiguration configuration) { this.commandManager = commandManager; + this.configuration = configuration; + this.autoScroll = configuration.LogAutoScroll; + this.openAtStartup = configuration.LogOpenAtStartup; SerilogEventSink.Instance.OnLogLine += Serilog_OnLogLine; } @@ -71,7 +76,14 @@ namespace Dalamud.Interface // Options menu if (ImGui.BeginPopup("Options")) { - ImGui.Checkbox("Auto-scroll", ref this.autoScroll); + if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) { + this.configuration.LogAutoScroll = this.autoScroll; + this.configuration.Save(); + }; + if (ImGui.Checkbox("Open at startup", ref this.openAtStartup)) { + this.configuration.LogOpenAtStartup = this.openAtStartup; + this.configuration.Save(); + }; ImGui.EndPopup(); }