diff --git a/FaloopIntegration/ActiveMobUi.cs b/FaloopIntegration/ActiveMobUi.cs deleted file mode 100644 index ca1f4b8b2..000000000 --- a/FaloopIntegration/ActiveMobUi.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Dalamud.Divination.Common.Api.Ui.Window; -using Dalamud.Game.Text; -using ImGuiNET; - -namespace Divination.FaloopIntegration; - -public class ActiveMobUi : IWindow, IDisposable -{ - private readonly ConcurrentDictionary mobs = new(); - private readonly Task cleanupTask; - private readonly CancellationTokenSource cancellation = new(); - - public ActiveMobUi() - { - cleanupTask = new Task(CleanUp); - cleanupTask.Start(); - } - - private bool isDrawing = true; - public bool IsDrawing - { - get => isDrawing && !mobs.IsEmpty; - set => isDrawing = value; - } - - public void Draw() - { - if (!IsDrawing) - { - return; - } - - if (ImGui.Begin(Localization.ActiveMob, ImGuiWindowFlags.AlwaysAutoResize)) - { - foreach (var mob in mobs.Values.OrderBy(x => x.SpawnedAt)) - { - DrawMob(mob); - } - - ImGui.End(); - } - } - - private static void DrawMob(MobSpawnEvent ev) - { - var span = DateTime.UtcNow - ev.SpawnedAt; - ImGui.Text( - $"{Utils.GetRankIconChar(ev.Rank).ToIconString()} {ev.Mob.Singular.RawString}{SeIconChar.CrossWorld.ToIconString()}{ev.World.Name.RawString} {span:mm\\:ss}"); - } - - public void OnMobSpawn(MobSpawnEvent ev) - { - mobs[ev.Id] = ev; - } - - public void OnMobDeath(MobDeathEvent ev) - { - mobs.TryRemove(ev.Id, out _); - } - - private async void CleanUp() - { - while (!cancellation.IsCancellationRequested) - { - foreach (var mob in mobs.Values.Where(x => DateTime.UtcNow - x.SpawnedAt > GetMaxAge(x))) - { - mobs.TryRemove(mob.Id, out _); - } - - await Task.Delay(10 * 1000); - } - } - - private static TimeSpan GetMaxAge(MobSpawnEvent ev) - { - return ev.Rank switch - { - MobRank.S => TimeSpan.FromMinutes(10), - MobRank.SS => TimeSpan.FromMinutes(10), - MobRank.FATE => TimeSpan.FromMinutes(30), - _ => throw new ArgumentOutOfRangeException(nameof(ev.Rank), ev.Rank, null), - }; - } - - public void Dispose() - { - cancellation.Cancel(); - } -} diff --git a/FaloopIntegration/Config/PluginConfigWindow.cs b/FaloopIntegration/Config/PluginConfigWindow.cs index f347b7686..9b68634d6 100644 --- a/FaloopIntegration/Config/PluginConfigWindow.cs +++ b/FaloopIntegration/Config/PluginConfigWindow.cs @@ -47,6 +47,11 @@ private void DrawPerRankConfigs() { ImGui.Indent(); + if (ImGuiEx.CheckboxConfig(Localization.EnableActiveMobUi, ref FaloopIntegration.Instance.Config.EnableActiveMobUi)) + { + FaloopIntegration.Instance.Ui.IsDrawing = FaloopIntegration.Instance.Config.EnableActiveMobUi; + } + ImGui.Checkbox(Localization.EnableSimpleReports, ref FaloopIntegration.Instance.Config.EnableSimpleReports); DrawPerRankConfig(Localization.RankS, ref Config.RankS); @@ -117,11 +122,6 @@ private static void DrawDebugConfig() { if (ImGui.CollapsingHeader("Debug")) { - if (ImGuiEx.CheckboxConfig(Localization.EnableActiveMobUi, ref FaloopIntegration.Instance.Config.EnableActiveMobUi)) - { - FaloopIntegration.Instance.Ui.IsDrawing = FaloopIntegration.Instance.Config.EnableActiveMobUi; - } - if (ImGui.Button("Emit mock payload")) { FaloopIntegration.Instance.EmitMockData(); diff --git a/FaloopIntegration/FaloopIntegration.cs b/FaloopIntegration/FaloopIntegration.cs index c1c4533d3..32399ca77 100644 --- a/FaloopIntegration/FaloopIntegration.cs +++ b/FaloopIntegration/FaloopIntegration.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Text.Json; using System.Threading.Tasks; using Dalamud.Divination.Common.Api.Dalamud; @@ -15,6 +16,8 @@ using Divination.FaloopIntegration.Config; using Divination.FaloopIntegration.Faloop; using Divination.FaloopIntegration.Faloop.Model; +using Divination.FaloopIntegration.Ipc; +using Divination.FaloopIntegration.Ui; using Lumina.Excel.GeneratedSheets; using SocketIOClient; @@ -27,7 +30,7 @@ public sealed class FaloopIntegration : DivinationPlugin(data.Data) ?? throw new InvalidOperationException("invalid spawn data"); - var ev = new MobSpawnEvent(mobData.BNpcId, worldId, spawn.ZoneId, data.ZoneInstance, spawn.ZonePoiIds?.FirstOrDefault(), mobData.Rank, spawn.Timestamp, spawn.Reporters?.FirstOrDefault()?.Name); + if (!FaloopEmbedData.Locations.TryGetValue(spawn.ZonePoiIds?.FirstOrDefault() ?? default, out var location)) + { + DalamudLog.Log.Debug("OnMobReport: location == null"); + } + + var ev = new MobSpawnEvent(mobData.BNpcId, worldId, spawn.ZoneId, data.ZoneInstance, mobData.Rank, spawn.Timestamp, spawn.Reporters?.FirstOrDefault()?.Name, location); OnMobSpawn(ev, config.Channel); DalamudLog.Log.Verbose("OnMobReport: OnSpawnMobReport"); break; @@ -112,8 +124,13 @@ private void OnMobReport(MobReportData data) case MobReportActions.SpawnLocation when config.EnableSpawnReport: { var spawn = JsonSerializer.Deserialize(data.Data) ?? throw new InvalidOperationException("invalid spawn location data"); + if (!FaloopEmbedData.Locations.TryGetValue(spawn.ZonePoiId, out var location)) + { + DalamudLog.Log.Debug("OnMobReport: location == null"); + } + var previous = Config.SpawnStates.FirstOrDefault(x => x.MobId == mobData.BNpcId && x.WorldId == worldId); - var ev = new MobSpawnEvent(mobData.BNpcId, worldId, spawn.ZoneId, data.ZoneInstance, spawn.ZonePoiId, mobData.Rank, previous?.SpawnedAt ?? DateTime.UtcNow, previous?.Reporter); + var ev = new MobSpawnEvent(mobData.BNpcId, worldId, spawn.ZoneId, data.ZoneInstance, mobData.Rank, previous?.SpawnedAt ?? DateTime.UtcNow, previous?.Reporter, location); OnMobSpawn(ev, config.Channel); break; } @@ -126,7 +143,7 @@ private void OnMobReport(MobReportData data) DalamudLog.Log.Debug("OnMobReport: previous == null"); break; } - var ev = new MobSpawnEvent(mobData.BNpcId, worldId, previous.TerritoryTypeId, data.ZoneInstance, previous.ZoneLocationId, mobData.Rank, spawn.Timestamp, previous.Reporter); + var ev = new MobSpawnEvent(mobData.BNpcId, worldId, previous.TerritoryTypeId, data.ZoneInstance, mobData.Rank, spawn.Timestamp, previous.Reporter, previous.Location); OnMobSpawn(ev, config.Channel); break; } @@ -152,6 +169,7 @@ private bool CheckSpawnNotificationCondition(PluginConfig.PerRankConfig config, var currentDataCenter = currentWorld?.DataCenter?.Value; if (currentWorld == default || currentDataCenter == default) { + // TODO DalamudLog.Log.Debug("OnMobReport: currentWorld == null || currentDataCenter == null"); return false; } @@ -197,13 +215,10 @@ private void OnMobSpawn(MobSpawnEvent ev, int channel) payloads.Add(new TextPayload($" {ev.Mob.Singular.RawString} ")); // append MapLink only if pop location is known - if (ev.ZoneLocationId.HasValue) + if (ev.Coordinates.HasValue) { - var mapLink = CreateMapLink(ev.TerritoryTypeId, ev.ZoneLocationId.Value, ev.ZoneInstance); - if (mapLink != default) - { - payloads.AddRange(mapLink.Payloads); - } + var mapLink = Utils.CreateMapLink(ev.TerritoryType, ev.Map, ev.Coordinates.Value, ev.ZoneInstance); + payloads.AddRange(mapLink.Payloads); } payloads.Add(new IconPayload(BitmapFontIcon.CrossWorld)); @@ -266,36 +281,6 @@ private void OnMobDeath(MobDeathEvent ev, int channel, bool skipOrphanReport) }); } - private SeString? CreateMapLink(uint zoneId, int zonePoiId, int? instance) - { - var zone = Dalamud.DataManager.GetExcelSheet()?.GetRow(zoneId); - var map = zone?.Map.Value; - if (zone == default || map == default) - { - DalamudLog.Log.Debug("CreateMapLink: zone == null || map == null"); - return default; - } - - if (!FaloopEmbedData.Locations.TryGetValue(zonePoiId, out var location)) - { - DalamudLog.Log.Debug("CreateMapLink: location == null"); - return default; - } - - var n = 41 / (map.SizeFactor / 100.0); - var loc = location.Split([','], 2) - .Select(int.Parse) - .Select(x => x / 2048.0 * n + 1) - .Select(x => Math.Round(x, 1)) - .Select(x => (float)x) - .ToList(); - - var mapLink = SeString.CreateMapLink(zone.RowId, zone.Map.Row, loc[0], loc[1]); - - var instanceIcon = Utils.GetInstanceIcon(instance); - return instanceIcon != default ? mapLink.Append(instanceIcon) : mapLink; - } - private static void OnAny(string name, SocketIOResponse response) { DalamudLog.Log.Debug("Event {Name} = {Message}", name, response); diff --git a/FaloopIntegration/FaloopIntegration.csproj b/FaloopIntegration/FaloopIntegration.csproj index f672ab88c..034efe7cb 100644 --- a/FaloopIntegration/FaloopIntegration.csproj +++ b/FaloopIntegration/FaloopIntegration.csproj @@ -32,4 +32,9 @@ all + + + + diff --git a/FaloopIntegration/Ipc/AetheryteLinkInChatIpc.cs b/FaloopIntegration/Ipc/AetheryteLinkInChatIpc.cs new file mode 100644 index 000000000..d2ce74738 --- /dev/null +++ b/FaloopIntegration/Ipc/AetheryteLinkInChatIpc.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Divination.Common.Api.Chat; +using Dalamud.Divination.Common.Api.Dalamud; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Divination.AetheryteLinkInChat.IpcModel; + +namespace Divination.FaloopIntegration.Ipc; + +public class AetheryteLinkInChatIpc(DalamudPluginInterface pluginInterface, IChatClient chatClient) +{ + private readonly ICallGateSubscriber subscriber = pluginInterface.GetIpcSubscriber(TeleportPayload.Name); + + public bool Teleport(uint territoryTypeId, uint mapId, Vector2 coordinates, uint worldId) + { + if (!IsPluginInstalled()) + { + chatClient.PrintError(Localization.AetheryteLinkInChatPluginNotInstalled); + return false; + } + + var payload = new TeleportPayload() + { + TerritoryTypeId = territoryTypeId, + MapId = mapId, + Coordinates = coordinates, + WorldId = worldId, + }; + + try + { + return subscriber.InvokeFunc(payload); + } + catch (Exception e) + { + DalamudLog.Log.Error(e, "failed to invoke Teleport"); + return false; + } + } + + private bool IsPluginInstalled() + { + return pluginInterface.InstalledPlugins.Any(x => x.Name == "Divination.AetheryteLinkInChat" && x.IsLoaded); + } +} diff --git a/FaloopIntegration/Localization.cs b/FaloopIntegration/Localization.cs index cbaa1fcfe..f1a6a464b 100644 --- a/FaloopIntegration/Localization.cs +++ b/FaloopIntegration/Localization.cs @@ -180,14 +180,14 @@ public static class Localization public static readonly LocalizedString ActiveMob = new() { - En = "Active Mobs", - Ja = "現在のモブ", + En = "Faloop: Active Mobs", + Ja = "Faloop: 現在のモブ情報", }; public static readonly LocalizedString EnableActiveMobUi = new() { En = "Enable Active Mobs UI", - Ja = "「現在のモブ」パネルを表示", + Ja = "「現在のモブ情報」パネルを表示", }; public static readonly LocalizedString EnableSimpleReports = new() @@ -195,4 +195,34 @@ public static class Localization En = "Enable simplified, condensed reports in chat", Ja = "簡素な通知メッセージを使用する", }; + + public static readonly LocalizedString TableHeaderMob = new() + { + En = "Mob", + Ja = "モブ", + }; + + public static readonly LocalizedString TableHeaderTime = new() + { + En = "Time", + Ja = "経過時間", + }; + + public static readonly LocalizedString TableButtonTeleport = new() + { + En = "Teleport", + Ja = "テレポ", + }; + + public static readonly LocalizedString TeleportingMessage = new() + { + En = "Teleporting to \"{0}\"...", + Ja = "「{0}」にテレポしています...", + }; + + public static readonly LocalizedString AetheryteLinkInChatPluginNotInstalled = new() + { + En = "Divination.AetheryteLinkInChat plugin is not installed.", + Ja = "Divination.AetheryteLinkInChat プラグインがインストールされていません。", + }; } diff --git a/FaloopIntegration/MobSpawnEvent.cs b/FaloopIntegration/MobSpawnEvent.cs index 2b427ff31..480e19fc3 100644 --- a/FaloopIntegration/MobSpawnEvent.cs +++ b/FaloopIntegration/MobSpawnEvent.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Numerics; using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; @@ -9,10 +11,10 @@ public record MobSpawnEvent( uint WorldId, uint TerritoryTypeId, int ZoneInstance, - int? ZoneLocationId, MobRank Rank, DateTime SpawnedAt, - string? Reporter) + string? Reporter, + string? Location) { [JsonIgnore] public BNpcName Mob => FaloopIntegration.Instance.Dalamud.DataManager.GetExcelSheet()?.GetRow(MobId) ?? throw new InvalidOperationException("invalid mob ID"); @@ -20,7 +22,30 @@ public record MobSpawnEvent( public World World => FaloopIntegration.Instance.Dalamud.DataManager.GetExcelSheet()?.GetRow(WorldId) ?? throw new InvalidOperationException("invalid world ID"); [JsonIgnore] public TerritoryType TerritoryType => FaloopIntegration.Instance.Dalamud.DataManager.GetExcelSheet()?.GetRow(TerritoryTypeId) ?? throw new InvalidOperationException("invalid territory type ID"); + [JsonIgnore] + public Map Map => TerritoryType.Map.Value ?? throw new InvalidOperationException("invalid map ID"); [JsonIgnore] public string Id => $"{MobId}_{WorldId}_{ZoneInstance}"; + + [JsonIgnore] + public Vector2? Coordinates + { + get + { + if (Location == default) + { + return default; + } + + var n = 41 / (Map.SizeFactor / 100.0); + var loc = Location.Split([','], 2) + .Select(int.Parse) + .Select(x => x / 2048.0 * n + 1) + .Select(x => Math.Round(x, 1)) + .Select(x => (float)x) + .ToList(); + return new Vector2(loc[0], loc[1]); + } + } } diff --git a/FaloopIntegration/Ui/ActiveMobUi.cs b/FaloopIntegration/Ui/ActiveMobUi.cs new file mode 100644 index 000000000..37271bcf3 --- /dev/null +++ b/FaloopIntegration/Ui/ActiveMobUi.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Divination.Common.Api.Chat; +using Dalamud.Divination.Common.Api.Dalamud; +using Dalamud.Divination.Common.Api.Ui.Window; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Divination.FaloopIntegration.Ipc; +using ImGuiNET; + +namespace Divination.FaloopIntegration.Ui; + +public class ActiveMobUi : IWindow, IDisposable +{ + private readonly ConcurrentDictionary mobs = new(); + private readonly Task cleanupTask; + private readonly CancellationTokenSource cancellation = new(); + private readonly AetheryteLinkInChatIpc ipc; + private readonly IChatClient chatClient; + + public ActiveMobUi(AetheryteLinkInChatIpc ipc, IChatClient chatClient) + { + this.ipc = ipc; + this.chatClient = chatClient; + cleanupTask = new Task(CleanUp); + cleanupTask.Start(); + } + + private bool isDrawing = true; + public bool IsDrawing + { + get => isDrawing && !mobs.IsEmpty; + set => isDrawing = value; + } + + public void Draw() + { + if (!IsDrawing) + { + return; + } + + if (ImGui.Begin(Localization.ActiveMob, ImGuiWindowFlags.AlwaysAutoResize)) + { + if (ImGui.BeginTable("active_mobs", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit)) + { + ImGui.TableSetupColumn(Localization.TableHeaderMob); + ImGui.TableSetupColumn(Localization.TableHeaderTime); + ImGui.TableSetupColumn(string.Empty); + ImGui.TableHeadersRow(); + + foreach (var mob in mobs.Values.OrderBy(x => x.SpawnedAt)) + { + ImGui.TableNextRow(); + DrawRow(mob); + } + + ImGui.EndTable(); + } + + ImGui.End(); + } + } + + private void DrawRow(MobSpawnEvent mob) + { + ImGui.TableNextColumn(); + var mobText = $"{Utils.GetRankIconChar(mob.Rank).ToIconChar()} {mob.Mob.Singular.RawString} {SeIconChar.CrossWorld.ToIconString()} {mob.World.Name.RawString}"; + ImGui.Text(mobText); + + ImGui.TableNextColumn(); + var span = DateTime.UtcNow - mob.SpawnedAt; + ImGui.Text(span.ToString("mm\\:ss")); + + ImGui.TableNextColumn(); + if (ImGui.Button($"{Localization.TableButtonTeleport}##{mob.Id}")) + { + if (ipc.Teleport(mob.TerritoryTypeId, mob.TerritoryType.Map.Row, mob.Coordinates ?? default, mob.WorldId)) + { + chatClient.Print(Localization.TeleportingMessage.Format(mobText)); + } + else + { + DalamudLog.Log.Warning("Failed to teleport: {Event}", mob); + } + } + } + + public void OnMobSpawn(MobSpawnEvent ev) + { + mobs[ev.Id] = ev; + } + + public void OnMobDeath(MobDeathEvent ev) + { + mobs.TryRemove(ev.Id, out _); + } + + private async void CleanUp() + { + while (!cancellation.IsCancellationRequested) + { + foreach (var mob in mobs.Values.Where(x => DateTime.UtcNow - x.SpawnedAt > GetMaxAge(x))) + { + mobs.TryRemove(mob.Id, out _); + } + + await Task.Delay(10 * 1000); + } + } + + private static TimeSpan GetMaxAge(MobSpawnEvent ev) + { + return ev.Rank switch + { + MobRank.S => TimeSpan.FromMinutes(10), + MobRank.SS => TimeSpan.FromMinutes(10), + MobRank.FATE => TimeSpan.FromMinutes(30), + _ => throw new ArgumentOutOfRangeException(nameof(ev.Rank), ev.Rank, null), + }; + } + + public void Dispose() + { + cancellation.Cancel(); + } +} diff --git a/FaloopIntegration/Utils.cs b/FaloopIntegration/Utils.cs index dc99b6f0b..e570750a5 100644 --- a/FaloopIntegration/Utils.cs +++ b/FaloopIntegration/Utils.cs @@ -1,7 +1,10 @@ using System; +using System.Numerics; using System.Text; using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Lumina.Excel.GeneratedSheets; namespace Divination.FaloopIntegration; @@ -74,4 +77,12 @@ public static string FormatTimeSpan(DateTime time) builder.Append(')'); return builder.ToString(); } + + public static SeString CreateMapLink(TerritoryType territoryType, Map map, Vector2 coordinates, int? instance) + { + var mapLink = SeString.CreateMapLink(territoryType.RowId, map.RowId, coordinates.X, coordinates.Y); + + var instanceIcon = GetInstanceIcon(instance); + return instanceIcon != default ? mapLink.Append(instanceIcon) : mapLink; + } } diff --git a/FaloopIntegration/packages.lock.json b/FaloopIntegration/packages.lock.json index e878fa019..f57bdc2a0 100644 --- a/FaloopIntegration/packages.lock.json +++ b/FaloopIntegration/packages.lock.json @@ -75,6 +75,9 @@ "System.Text.Encodings.Web": "8.0.0" } }, + "aetherytelinkinchat.ipcmodel": { + "type": "Project" + }, "Dalamud.Divination.Common": { "type": "Project" }