Plugin.cs 12 KB

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