Plugin.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Numerics;
  7. using System.Threading.Tasks;
  8. using Dalamud.Interface.Windowing;
  9. using Dalamud.Plugin;
  10. using Dalamud.Plugin.Services;
  11. using Dalamud.Utility;
  12. using HuntBuddy.Attributes;
  13. using HuntBuddy.Ipc;
  14. using HuntBuddy.Structs;
  15. using HuntBuddy.Windows;
  16. using ImGuiNET;
  17. using Lumina.Excel;
  18. using Lumina.Excel.Sheets;
  19. using Lumina.Text;
  20. using Lumina.Text.ReadOnly;
  21. namespace HuntBuddy;
  22. public class Plugin: IDalamudPlugin {
  23. public string Name => "Hunt Buddy";
  24. private readonly PluginCommandManager<Plugin> commandManager;
  25. private ObtainedBillEnum lastState;
  26. // Dictionary<string ExpansionName, Dictionary<KeyValuePair<uint MobTerritoryType, string MobTerritoryName>, List<MobHuntEntry MobsInZone>>>
  27. public readonly Dictionary<string, Dictionary<KeyValuePair<uint, string>, List<MobHuntEntry>>> MobHuntEntries;
  28. public readonly ConcurrentBag<MobHuntEntry> CurrentAreaMobHuntEntries;
  29. public bool MobHuntEntriesReady = true;
  30. public readonly unsafe MobHuntStruct* MobHuntStruct;
  31. public readonly Configuration Configuration;
  32. public static TeleportConsumer? TeleportConsumer { get; private set; }
  33. public static EspConsumer? EspConsumer { get; private set; }
  34. private WindowSystem WindowSystem {
  35. get;
  36. }
  37. internal MainWindow MainWindow {
  38. get;
  39. }
  40. private ConfigurationWindow ConfigurationWindow {
  41. get;
  42. }
  43. public static Plugin Instance {
  44. get;
  45. internal set;
  46. } = null!;
  47. public Plugin(IDalamudPluginInterface pluginInterface) {
  48. Instance = this;
  49. pluginInterface.Create<Service>();
  50. this.commandManager = new PluginCommandManager<Plugin>(this, Service.Commands);
  51. this.MobHuntEntries = [];
  52. this.CurrentAreaMobHuntEntries = [];
  53. this.Configuration = (Configuration)(Service.PluginInterface.GetPluginConfig() ?? new Configuration());
  54. this.Configuration.IconBackgroundColourU32 =
  55. ImGui.ColorConvertFloat4ToU32(this.Configuration.IconBackgroundColour);
  56. unsafe {
  57. this.MobHuntStruct =
  58. (MobHuntStruct*)Service.SigScanner.GetStaticAddressFromSig(
  59. "48 8D 0D ?? ?? ?? ?? 8B D8 0F B6 52");
  60. }
  61. this.MainWindow = new MainWindow();
  62. this.ConfigurationWindow = new ConfigurationWindow();
  63. this.WindowSystem = new WindowSystem("HuntBuddy");
  64. this.WindowSystem.AddWindow(this.MainWindow);
  65. this.WindowSystem.AddWindow(new LocalHuntsWindow());
  66. this.WindowSystem.AddWindow(this.ConfigurationWindow);
  67. Plugin.TeleportConsumer = new TeleportConsumer();
  68. Plugin.EspConsumer = new EspConsumer();
  69. Service.ClientState.TerritoryChanged += this.ClientStateOnTerritoryChanged;
  70. Service.PluginInterface.UiBuilder.Draw += this.WindowSystem.Draw;
  71. Service.PluginInterface.UiBuilder.OpenConfigUi += this.OpenConfigUi;
  72. Service.PluginInterface.UiBuilder.OpenMainUi += this.OpenMainUi;
  73. Service.Framework.Update += this.FrameworkOnUpdate;
  74. }
  75. private unsafe void FrameworkOnUpdate(IFramework framework) {
  76. if (this.lastState == this.MobHuntStruct->ObtainedBillEnumFlags) {
  77. return;
  78. }
  79. this.lastState = this.MobHuntStruct->ObtainedBillEnumFlags;
  80. this.PluginCommand(string.Empty, "reload");
  81. }
  82. private void ClientStateOnTerritoryChanged(ushort e) {
  83. this.CurrentAreaMobHuntEntries.Clear();
  84. foreach (MobHuntEntry mobHuntEntry in this.MobHuntEntries.SelectMany(
  85. expansionEntry => expansionEntry.Value
  86. .Where(entry => entry.Key.Key == Service.ClientState.TerritoryType)
  87. .SelectMany(entry => entry.Value))) {
  88. this.CurrentAreaMobHuntEntries.Add(mobHuntEntry);
  89. }
  90. }
  91. private void DrawInterface() => this.MainWindow.Toggle();
  92. public void OpenConfigUi() => this.ConfigurationWindow.Toggle();
  93. public void OpenMainUi() => this.MainWindow.Toggle();
  94. private void Dispose(bool disposing) {
  95. if (!disposing) {
  96. return;
  97. }
  98. this.MobHuntEntriesReady = false;
  99. Service.ClientState.TerritoryChanged -= this.ClientStateOnTerritoryChanged;
  100. Service.Framework.Update -= this.FrameworkOnUpdate;
  101. Service.PluginInterface.UiBuilder.Draw -= this.WindowSystem.Draw;
  102. Service.PluginInterface.UiBuilder.OpenConfigUi -= this.OpenConfigUi;
  103. Service.PluginInterface.UiBuilder.OpenMainUi -= this.OpenMainUi;
  104. this.WindowSystem.RemoveAllWindows();
  105. this.commandManager.Dispose();
  106. }
  107. [Command("/phb")]
  108. [HelpMessage(
  109. "Toggles UI\nArguments:\nreload - Reloads data\nlocal - Toggles the local hunt marks window\nnext - Flags the next hunt target to find\nlist - list all hunt targets by expansion")]
  110. public unsafe void PluginCommand(string command, string args) {
  111. try {
  112. switch (args.Trim().ToLower()) {
  113. case "reload":
  114. this.MobHuntEntriesReady = false;
  115. Task.Run(this.ReloadData);
  116. break;
  117. case "local":
  118. this.Configuration.ShowLocalHunts = !this.Configuration.ShowLocalHunts;
  119. this.Configuration.Save();
  120. break;
  121. case "next":
  122. if (this.MobHuntEntries.Count > 0) {
  123. bool filterPredicate(MobHuntEntry entry) => entry.IsEliteMark ||
  124. this.MobHuntStruct->CurrentKills[
  125. entry.CurrentKillsOffset] <
  126. entry.NeededKills;
  127. Location.OpenType openType = Location.OpenType.None;
  128. Vector3 playerLocation = Service.ClientState.LocalPlayer!.Position;
  129. Map map = Service.DataManager.GetExcelSheet<TerritoryType>()!.GetRow(Service.ClientState
  130. .TerritoryType)!.Map!.Value!;
  131. Vector2 playerVec2 = MapUtil.WorldToMap(new Vector2(playerLocation.X, playerLocation.Z), map);
  132. MobHuntEntry? chosen = this.CurrentAreaMobHuntEntries
  133. .Where(filterPredicate)
  134. .OrderBy(entry =>
  135. entry.IsEliteMark
  136. ? float.MaxValue
  137. : Vector2.Distance(Location.Database[entry.MobHuntId].Coordinate, playerVec2))
  138. .FirstOrDefault();
  139. if (chosen == null) {
  140. Service.PluginLog.Information("No marks in current zone, looking in current expansion");
  141. openType = this.Configuration.IncludeAreaOnMap
  142. ? Location.OpenType.ShowOpen
  143. : Location.OpenType.MarkerOpen;
  144. string expansion = Service.DataManager.GetExcelSheet<TerritoryType>()
  145. .GetRow(Service.ClientState.TerritoryType).ExVersion.Value.Name.ToString();
  146. Service.PluginLog.Information(
  147. $"Player is in a zone from {expansion}; known expansions are {string.Join(", ", this.MobHuntEntries.Keys)}");
  148. List<MobHuntEntry> candidates =
  149. this.MobHuntEntries.TryGetValue(expansion,
  150. out Dictionary<KeyValuePair<uint, string>, List<MobHuntEntry>>? entry)
  151. ? entry.Values.SelectMany(l => l).Where(filterPredicate).ToList()
  152. : [];
  153. // if we didn't find any candidates, we try a different method to fill it
  154. if (candidates.Count == 0) {
  155. Service.PluginLog.Information(
  156. "Nothing available in current expansion, looking globally");
  157. candidates =
  158. this.MobHuntEntries.Values
  159. .SelectMany(dict => dict.Values)
  160. .SelectMany(l => l)
  161. .Where(filterPredicate)
  162. .ToList();
  163. }
  164. // regardless of HOW we got our candidates, assuming we did in fact get them, we pick one
  165. // note that this can't be merged into the above block because the above MAY run, and if so MUST run first,
  166. // but this block must ALWAYS run, regardless
  167. if (candidates.Count >= 1) {
  168. Service.PluginLog.Information($"Found {candidates.Count}");
  169. chosen = candidates[new Random().Next(candidates.Count)];
  170. }
  171. }
  172. if (chosen != null) {
  173. if (chosen.IsEliteMark) {
  174. Service.Chat.Print($"Hunting elite mark {chosen.Name} in {chosen.TerritoryName}");
  175. if (!this.Configuration.SuppressEliteMarkLocationWarning) {
  176. Service.Chat.Print("Elite mark spawn locations are not available, since there are so many possibilities and the mob will only ever be in one place at a time."
  177. + "\n(You can suppress this warning in the plugin settings)");
  178. }
  179. }
  180. else {
  181. long remaining = chosen.NeededKills -
  182. this.MobHuntStruct->CurrentKills[chosen.CurrentKillsOffset];
  183. Service.Chat.Print($"Hunting {remaining}x {chosen.Name} in {chosen.TerritoryName}");
  184. Location.CreateMapMarker(
  185. chosen.TerritoryType,
  186. chosen.MapId,
  187. chosen.MobHuntId,
  188. chosen.Name,
  189. openType);
  190. }
  191. if (this.Configuration.EnableXivEspIntegration && this.Configuration.AutoSetEspSearchOnNextHuntCommand && Plugin.EspConsumer?.IsAvailable == true)
  192. Plugin.EspConsumer.SearchFor(chosen.Name!);
  193. }
  194. else {
  195. Service.PluginLog.Information("Unable to find a hunt mark to target");
  196. Service.Chat.Print(
  197. "Couldn't find any hunt marks. Either you have no bills, or this is a bug.");
  198. }
  199. }
  200. break;
  201. case "ls":
  202. case "list":
  203. if (this.MobHuntEntries.Count < 1) {
  204. Service.Chat.Print(
  205. "No hunt marks found. If this doesn't sound right, please use `/phb reload` and try again.");
  206. break;
  207. }
  208. foreach (string expac in this.MobHuntEntries.Keys) {
  209. Service.Chat.Print(
  210. $"{expac}: {string.Join(", ", this.MobHuntEntries[expac].Values.SelectMany(e => e).OrderBy(s => s.Name).Select(m => m.Name))}");
  211. }
  212. break;
  213. default:
  214. this.DrawInterface();
  215. break;
  216. }
  217. }
  218. catch (Exception e) {
  219. Service.PluginLog.Error($"Error in command handler: {e}");
  220. }
  221. }
  222. public unsafe void ReloadData() {
  223. this.MobHuntEntries.Clear();
  224. List<MobHuntEntry> mobHuntList = [];
  225. SubrowExcelSheet<MobHuntOrder> mobHuntOrderSheet = Service.DataManager.GetSubrowExcelSheet<MobHuntOrder>();
  226. foreach (BillEnum billNumber in Enum.GetValues<BillEnum>()) {
  227. if (!this.MobHuntStruct->ObtainedBillEnumFlags.HasFlag((ObtainedBillEnum)(1 << (int)billNumber))) {
  228. continue;
  229. }
  230. MobHuntOrderType mobHuntOrderTypeRow =
  231. Service.DataManager.Excel.GetSheet<MobHuntOrderType>()!.GetRow((uint)billNumber)!;
  232. uint rowId = mobHuntOrderTypeRow.OrderStart.Value!.RowId +
  233. (uint)(this.MobHuntStruct->BillOffset[mobHuntOrderTypeRow.RowId] - 1);
  234. IEnumerable<MobHuntOrder> mobHuntOrderRows = mobHuntOrderSheet[rowId];
  235. foreach (MobHuntOrder mobHuntOrderRow in mobHuntOrderRows) {
  236. MobHuntEntry? mobHuntEntry =
  237. mobHuntList.FirstOrDefault(x => x.MobHuntId == mobHuntOrderRow.Target.Value.Name.RowId);
  238. if (mobHuntEntry == null) {
  239. mobHuntList.Add(
  240. new MobHuntEntry {
  241. Name = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(
  242. mobHuntOrderRow.Target.Value.Name.Value.Singular.ToString()),
  243. TerritoryName =
  244. mobHuntOrderRow.Target.Value.TerritoryType.Value.PlaceName.Value.Name.ToString(),
  245. ExpansionName = mobHuntOrderRow.Target.Value.TerritoryType.Value.TerritoryType.Value
  246. .ExVersion.Value.Name.ToString(),
  247. ExpansionId = mobHuntOrderRow.Target.Value.TerritoryType.Value.TerritoryType.Value
  248. .ExVersion.RowId,
  249. MapId = mobHuntOrderRow.Target.Value.TerritoryType.RowId,
  250. TerritoryType = mobHuntOrderRow.Target.Value.TerritoryType.Value.TerritoryType.RowId,
  251. MobHuntId = mobHuntOrderRow.Target.Value.Name.RowId,
  252. IsEliteMark = mobHuntOrderTypeRow.Type == 2,
  253. CurrentKillsOffset = (5 * (uint)billNumber) + mobHuntOrderRow.SubrowId,
  254. NeededKills = mobHuntOrderRow.NeededKills,
  255. Icon = mobHuntOrderRow.Target.Value.Icon,
  256. });
  257. }
  258. else {
  259. if (mobHuntEntry.NeededKills < mobHuntOrderRow.NeededKills) {
  260. mobHuntEntry.NeededKills = mobHuntOrderRow.NeededKills;
  261. }
  262. }
  263. }
  264. }
  265. foreach (MobHuntEntry entry in mobHuntList) {
  266. string key = entry.ExpansionName ?? "Unknown";
  267. KeyValuePair<uint, string> subKey =
  268. new(entry.TerritoryType, entry.TerritoryName ?? "Unknown");
  269. if (!this.MobHuntEntries.ContainsKey(key)) {
  270. this.MobHuntEntries[key] = [];
  271. }
  272. if (!this.MobHuntEntries[key].ContainsKey(subKey)) {
  273. this.MobHuntEntries[key][subKey] = [];
  274. }
  275. this.MobHuntEntries[key][subKey].Add(entry);
  276. }
  277. this.ClientStateOnTerritoryChanged(0);
  278. this.MobHuntEntriesReady = true;
  279. }
  280. public void Dispose() {
  281. this.Dispose(true);
  282. GC.SuppressFinalize(this);
  283. }
  284. }