using System; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Configuration; using Dalamud.Game.ClientState.Conditions; using Dalamud.IoC; using Dalamud.Plugin; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI; namespace AutoFishingStool; public sealed unsafe class Plugin : IDalamudPlugin { private enum PostureAction { None, Sit, Stand, } private const int SitDelayMilliseconds = 2500; private const int ActionSettleMilliseconds = 500; private const uint FisherClassJobId = 18; [PluginService] internal static IDalamudPluginInterface PluginInterface { get; private set; } = null!; [PluginService] internal static ICondition Condition { get; private set; } = null!; [PluginService] internal static IFramework Framework { get; private set; } = null!; [PluginService] internal static IPlayerState PlayerState { get; private set; } = null!; [PluginService] internal static IPluginLog Log { get; private set; } = null!; private readonly Configuration configuration; private DateTime? postureAt; private PostureAction pendingPosture; private bool satByPlugin; private bool stoodForSpectral; private bool spectralCurrentActive; private bool waitingForActionToFinish; private bool configWindowOpen; public Plugin() { configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); spectralCurrentActive = IsSpectralCurrentActive(); Condition.ConditionChange += OnConditionChange; Framework.Update += OnFrameworkUpdate; PluginInterface.UiBuilder.Draw += DrawConfigWindow; PluginInterface.UiBuilder.OpenConfigUi += OpenConfigWindow; PluginInterface.UiBuilder.OpenMainUi += OpenConfigWindow; Log.Information("Auto Fishing Stool loaded"); } private void OnConditionChange(ConditionFlag flag, bool value) { if (flag == ConditionFlag.Fishing) { Log.Information("Fishing condition changed to {Value}", value); if (value && Condition[ConditionFlag.Gathering] && IsFisher() && !satByPlugin && !ShouldStandForSpectral()) { SchedulePosture(PostureAction.Sit, SitDelayMilliseconds); Log.Information("Fishing became active; stable Sit timer started"); } else if (!value && !satByPlugin) { CancelPendingPosture(); Log.Information("Fishing became inactive; stable Sit timer reset"); } return; } if (flag != ConditionFlag.Gathering) return; Log.Information("Gathering condition changed to {Value}", value); if (value) { if (!IsFisher()) return; satByPlugin = false; stoodForSpectral = false; CancelPendingPosture(); Log.Information("Fisher gathering session started"); return; } satByPlugin = false; stoodForSpectral = false; CancelPendingPosture(); } private void OnFrameworkUpdate(IFramework framework) { UpdateSpectralCurrentState(); if (pendingPosture == PostureAction.None || postureAt is null) return; if (!Condition[ConditionFlag.Gathering] || !Condition[ConditionFlag.Fishing] || !IsFisher()) { CancelPendingPosture(); return; } if (pendingPosture == PostureAction.Sit && ShouldStandForSpectral()) { CancelPendingPosture(); return; } if (pendingPosture == PostureAction.Stand && !ShouldStandForSpectral()) { CancelPendingPosture(); return; } if (DateTime.UtcNow < postureAt.Value) return; if (Condition[ConditionFlag.ExecutingGatheringAction]) { if (!waitingForActionToFinish) { waitingForActionToFinish = true; Log.Information("Sit timer elapsed during a fishing action; waiting for it to finish"); } return; } if (waitingForActionToFinish) { waitingForActionToFinish = false; postureAt = DateTime.UtcNow.AddMilliseconds(ActionSettleMilliseconds); Log.Information("Fishing action finished; posture command scheduled after settling"); return; } var completedPosture = pendingPosture; var uiModule = UIModule.Instance(); if (uiModule is null) { Log.Warning("Could not access the game's UI module"); CancelPendingPosture(); return; } var command = Utf8String.FromString("/sit"); try { uiModule->ProcessChatBoxEntry(command); Log.Information("Submitted /sit to {Posture}", completedPosture); } finally { command->Dtor(true); } if (completedPosture == PostureAction.Sit) { satByPlugin = true; stoodForSpectral = false; } else { satByPlugin = false; stoodForSpectral = true; } CancelPendingPosture(); } private void UpdateSpectralCurrentState() { var active = IsSpectralCurrentActive(); if (active == spectralCurrentActive) return; spectralCurrentActive = active; Log.Information("Spectral Current changed to {Value}", active); if (!configuration.StandDuringSpectralCurrents) return; if (active) { if (pendingPosture == PostureAction.Sit) CancelPendingPosture(); if (satByPlugin) SchedulePosture(PostureAction.Stand, 0); return; } if (stoodForSpectral && Condition[ConditionFlag.Gathering] && Condition[ConditionFlag.Fishing] && IsFisher()) SchedulePosture(PostureAction.Sit, ActionSettleMilliseconds); } private static bool IsSpectralCurrentActive() { try { var eventFramework = EventFramework.Instance(); var oceanFishing = eventFramework is null ? null : eventFramework->GetInstanceContentOceanFishing(); return oceanFishing is not null && oceanFishing->SpectralCurrentActive; } catch (Exception exception) { Log.Warning(exception, "Could not read Ocean Fishing state"); return false; } } private bool ShouldStandForSpectral() { return configuration.StandDuringSpectralCurrents && spectralCurrentActive; } private void SchedulePosture(PostureAction posture, int delayMilliseconds) { pendingPosture = posture; postureAt = DateTime.UtcNow.AddMilliseconds(delayMilliseconds); waitingForActionToFinish = false; } private void CancelPendingPosture() { pendingPosture = PostureAction.None; postureAt = null; waitingForActionToFinish = false; } private void OpenConfigWindow() { configWindowOpen = true; } private void DrawConfigWindow() { if (!configWindowOpen) return; ImGui.SetNextWindowSize(new Vector2(430, 0), ImGuiCond.FirstUseEver); if (!ImGui.Begin("Auto Fishing Stool Settings###AutoFishingStoolSettings", ref configWindowOpen, ImGuiWindowFlags.AlwaysAutoResize)) { ImGui.End(); return; } var standDuringSpectralCurrents = configuration.StandDuringSpectralCurrents; if (ImGui.Checkbox("Stand during Spectral Currents", ref standDuringSpectralCurrents)) { configuration.StandDuringSpectralCurrents = standDuringSpectralCurrents; PluginInterface.SavePluginConfig(configuration); ApplySpectralSettingChange(); } ImGui.TextWrapped("Stand while a Spectral Current is active, then sit again when it ends."); ImGui.Separator(); ImGui.Text($"Spectral Current detected: {(spectralCurrentActive ? "Yes" : "No")}"); ImGui.End(); } private void ApplySpectralSettingChange() { if (!spectralCurrentActive) return; if (configuration.StandDuringSpectralCurrents) { if (pendingPosture == PostureAction.Sit) CancelPendingPosture(); if (satByPlugin) SchedulePosture(PostureAction.Stand, 0); return; } if (pendingPosture == PostureAction.Stand) CancelPendingPosture(); if (stoodForSpectral && Condition[ConditionFlag.Gathering] && Condition[ConditionFlag.Fishing] && IsFisher()) SchedulePosture(PostureAction.Sit, ActionSettleMilliseconds); } private static bool IsFisher() { return PlayerState.IsLoaded && PlayerState.ClassJob.RowId == FisherClassJobId; } public void Dispose() { Condition.ConditionChange -= OnConditionChange; Framework.Update -= OnFrameworkUpdate; PluginInterface.UiBuilder.Draw -= DrawConfigWindow; PluginInterface.UiBuilder.OpenConfigUi -= OpenConfigWindow; PluginInterface.UiBuilder.OpenMainUi -= OpenConfigWindow; } }