diff --git a/AutoRetainerAPI b/AutoRetainerAPI index 75a0db1..6f0aaa5 160000 --- a/AutoRetainerAPI +++ b/AutoRetainerAPI @@ -1 +1 @@ -Subproject commit 75a0db11d7cc982b54f9cacb8a2b9c17b023b718 +Subproject commit 6f0aaa55bce6ec79fd4d72f84f21597b39e5445d diff --git a/AutoShutdown/AutoShutdown.csproj b/AutoShutdown/AutoShutdown.csproj index dfffaf5..cd32e82 100644 --- a/AutoShutdown/AutoShutdown.csproj +++ b/AutoShutdown/AutoShutdown.csproj @@ -1,8 +1,8 @@ - net7.0-windows + net8.0-windows 0.1 - 11.0 + 12 enable true false @@ -13,6 +13,7 @@ $(SolutionDir)=X:\ true portable + true diff --git a/AutoShutdown/Plogon.cs b/AutoShutdown/Plogon.cs index 23189a6..f4c489a 100644 --- a/AutoShutdown/Plogon.cs +++ b/AutoShutdown/Plogon.cs @@ -1,74 +1,159 @@ using System; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Linq; using System.Reflection; +using System.Text; +using System.Threading.Tasks; using System.Windows.Forms; using AutoRetainerAPI; +using Dalamud; +using Dalamud.Game; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Memory; using Dalamud.Plugin; using Dalamud.Plugin.Services; using ECommons; using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; using LLib; +using LLib.GameUI; +using Task = System.Threading.Tasks.Task; namespace AutoShutdown; public sealed class Plogon : IDalamudPlugin { + private readonly nint _skipMovieAddress; + private readonly byte[] _skipMovieOriginalBytes; private readonly AutoRetainerApi _autoRetainerApi; private readonly DalamudReflector _reflector; private readonly IPluginLog _pluginLog; private readonly IClientState _clientState; + private readonly IGameGui _gameGui; + private readonly IFramework _framework; public Plogon(DalamudPluginInterface pluginInterface, IFramework framework, IPluginLog pluginLog, - IClientState clientState) + IClientState clientState, IGameGui gameGui, ISigScanner sigScanner) { + _skipMovieAddress = sigScanner.ScanText("48 01 8E ?? ?? ?? ?? 48 81 BE ?? ?? ?? ?? 60 EA 00 00"); + SafeMemory.ReadBytes(_skipMovieAddress, 7, out _skipMovieOriginalBytes); + ECommonsMain.Init(pluginInterface, this); _autoRetainerApi = new AutoRetainerApi(); _reflector = new DalamudReflector(pluginInterface, framework, pluginLog); + + _framework = framework; _pluginLog = pluginLog; _clientState = clientState; + _gameGui = gameGui; - _clientState.Logout += SetupShutdown; + _clientState.Logout += OnLogout; + _framework.Update += FrameworkUpdate; - framework.RunOnTick(() => SetupShutdown(), TimeSpan.FromSeconds(10)); + _framework.RunOnTick(MoveToBackground, TimeSpan.FromSeconds(10)); + + // we don't want to watch movies, regardless of how long we're gone - 7 byte NOP for the 'add [rsi+1110h], rcx' + SafeMemory.WriteBytes(_skipMovieAddress, [0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00]); + unsafe + { + var lobby = AgentLobby.Instance(); + if (lobby != null) + lobby->IdleTime = 0; + } } - private long StartedAt { get; } = Environment.TickCount64; - private long ShutdownAt => StartedAt + (long)TimeSpan.FromDays(2).TotalMilliseconds; + private DateTime ShutdownAt { get; } = DateTime.Now.AddDays(2); - private void SetupShutdown() + private unsafe void FrameworkUpdate(IFramework _) + { + if (DateTime.Now > ShutdownAt && _gameGui.TryGetAddonByName("_TitleMenu", out var addon)) + { + Environment.Exit(0); + } + } + + private bool IsMultiAndNightMode() { if (_autoRetainerApi.Ready && _reflector.TryGetDalamudPlugin("AutoRetainer", out var autoRetainer, false, true)) { - var shutdown = autoRetainer.GetType().Assembly.GetType("AutoRetainer.Modules.Shutdown")!; + var mm = autoRetainer.GetType().Assembly.GetType("AutoRetainer.Modules.Multi.MultiMode")!; + bool multi = (bool)mm.GetProperty("Active", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null)!; - var shutdownAt = shutdown.GetField("ShutdownAt", BindingFlags.Static | BindingFlags.NonPublic)!; - var forceShutdownAt = shutdown.GetField("ForceShutdownAt", BindingFlags.Static | BindingFlags.NonPublic)!; + var config = + autoRetainer.GetType().GetProperty("C", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null)!; + bool nightMode = (bool)config.GetType().GetField("NightMode", BindingFlags.Instance | BindingFlags.Public)! + .GetValue(config)!; - var currentShutdownAt = (long?)shutdownAt.GetValue(null) ?? 0; - if (currentShutdownAt == 0) - { - _pluginLog.Information( - $"Setting shutdown date to {new DateTime(TimeSpan.FromMilliseconds(ShutdownAt).Ticks)}"); - shutdownAt.SetValue(null, ShutdownAt); - forceShutdownAt.SetValue(null, ShutdownAt + (long)TimeSpan.FromMinutes(10).TotalMilliseconds); - } - else - { - _pluginLog.Information( - $"Shutdown is already set to {new DateTime(TimeSpan.FromMilliseconds(currentShutdownAt).Ticks)}"); - } + //_pluginLog.Information($"AR: multi={multi}, night={nightMode}"); + + return multi && nightMode; } - // move to background using alt+esc - unsafe + return false; + } + + private void MoveToBackground() + { + if (IsMultiAndNightMode()) { - if (!Framework.Instance()->WindowInactive) - SendKeys.SendWait("%({esc})"); + // move to background using alt+esc + unsafe + { + if (!Framework.Instance()->WindowInactive) + SendKeys.SendWait("%({esc})"); + } } } + private void OnLogout() + { + if (IsMultiAndNightMode()) + { + List nextVesselTimes = + _autoRetainerApi.GetRegisteredCharacters().SelectMany(localContentId => + { + var data = _autoRetainerApi.GetOfflineCharacterData(localContentId); + return data.OfflineSubmarineData.Where(x => data.EnabledSubs.Contains(x.Name)) + .Select(x => TimeSpan.FromSeconds(x.ReturnTime - Framework.GetServerTime())) + .Select(x => x <= TimeSpan.Zero ? TimeSpan.Zero : x) + .ToList(); + }) + .ToList(); + if (nextVesselTimes.Count > 0) + { + TimeSpan next = nextVesselTimes.Min(); + _pluginLog.Information($"Next vessel time: {next}"); + Task.Factory.StartNew(async () => + { + try + { + await using NamedPipeClientStream pipe = + new NamedPipeClientStream(".", "ffxiv_TheWatcher", PipeDirection.Out); + await pipe.ConnectAsync(1000); + + byte[] content = Encoding.UTF8.GetBytes($"{next.TotalSeconds}"); + await pipe.WriteAsync(content, 0, content.Length); + } + catch (Exception e) + { + _pluginLog.Warning(e, "Unable to send IPC"); + } + }, TaskCreationOptions.LongRunning); + } + } + + MoveToBackground(); + } + public void Dispose() { - _clientState.Logout -= SetupShutdown; + SafeMemory.WriteBytes(_skipMovieAddress, _skipMovieOriginalBytes); + _framework.Update -= FrameworkUpdate; + _clientState.Logout -= OnLogout; + _reflector.Dispose(); _autoRetainerApi.Dispose(); ECommonsMain.Dispose(); diff --git a/AutoShutdown/packages.lock.json b/AutoShutdown/packages.lock.json index 84c5a5c..e25d14a 100644 --- a/AutoShutdown/packages.lock.json +++ b/AutoShutdown/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - "net7.0-windows7.0": { + "net8.0-windows7.0": { "DalamudPackager": { "type": "Direct", "requested": "[2.1.12, )", diff --git a/ECommons b/ECommons index f1c688a..d238d41 160000 --- a/ECommons +++ b/ECommons @@ -1 +1 @@ -Subproject commit f1c688a0599b41d70230021328a575da7351cf91 +Subproject commit d238d4188e8b47b11252d75cb5e4b678b8da2756 diff --git a/LLib b/LLib index 865a608..3792244 160000 --- a/LLib +++ b/LLib @@ -1 +1 @@ -Subproject commit 865a6080319f8ccbcd5fd5b0004404822b6e60d4 +Subproject commit 3792244261a9f5426a7916f5a6dd1966238ba84a diff --git a/TheWatcher/NativeMethods.cs b/TheWatcher/NativeMethods.cs new file mode 100644 index 0000000..2b153bc --- /dev/null +++ b/TheWatcher/NativeMethods.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; + +namespace TheWatcher; + +internal static class NativeMethods +{ + [DllImport("powrprof.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent); + + public delegate void TimerCompleteDelegate(); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr CreateWaitableTimer(IntPtr lpTimerAttributes, bool bManualReset, string lpTimerName); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetWaitableTimer(IntPtr hTimer, [In] ref long pDueTime, int lPeriod, + TimerCompleteDelegate? pfnCompletionRoutine, IntPtr pArgToCompletionRoutine, bool fResume); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CancelWaitableTimer(IntPtr hTimer); +} diff --git a/TheWatcher/Program.cs b/TheWatcher/Program.cs index afc3560..3c6f15f 100644 --- a/TheWatcher/Program.cs +++ b/TheWatcher/Program.cs @@ -1,25 +1,28 @@ using System.Diagnostics; +using System.IO.Pipes; +using System.Runtime.InteropServices; +using System.Text; namespace TheWatcher; internal sealed class Program { + private static CancellationToken CancellationToken { get; set; } + private static bool EnableSleep { get; set; } + public static async Task Main(string[] args) { Console.WriteLine("o.O"); using CancellationTokenSource cts = new CancellationTokenSource(); + CancellationToken = cts.Token; + + _ = Task.Factory.StartNew(async () => { await RunServer(); }, TaskCreationOptions.LongRunning); + Console.CancelKeyPress += (_, e) => { - try - { - // ReSharper disable once AccessToDisposedClosure - cts.Cancel(); - } - catch (ObjectDisposedException) - { - } - + EnableSleep = !EnableSleep; + Console.WriteLine($"[{DateTime.Now}] Sleep => {EnableSleep}"); e.Cancel = true; }; @@ -99,4 +102,102 @@ internal sealed class Program } } } + + private static void SleepUntil(DateTime dt) + { + Console.WriteLine($"[{DateTime.Now}] Sleeping until {dt}..."); + + nint wakeTimer = SetWakeAt(dt); + if (wakeTimer != nint.Zero) + { + bool suspended = NativeMethods.SetSuspendState(false, false, false); + if (suspended) + { + if (DateTime.Now < dt) + { + Console.WriteLine($"[{DateTime.Now}] Resumed from suspend, but expected to sleep until {dt} => deactivating sleep"); + EnableSleep = false; + } + else + { + Console.WriteLine($"[{DateTime.Now}] Resumed from suspend"); + } + } + else + Console.WriteLine( + $"[{DateTime.Now}] Not Suspended / {Marshal.GetLastPInvokeError()} - {Marshal.GetLastPInvokeErrorMessage()}"); + NativeMethods.CancelWaitableTimer(wakeTimer); + } + else + { + Console.WriteLine( + $"[{DateTime.Now}] Not able to set wait timer / {Marshal.GetLastPInvokeError()} - {Marshal.GetLastPInvokeErrorMessage()}"); + } + } + + private static IntPtr SetWakeAt(DateTime dt) + { + NativeMethods.TimerCompleteDelegate? timerComplete = null; + + // read the manual for SetWaitableTimer to understand how this number is interpreted. + long interval = dt.ToFileTimeUtc(); + IntPtr handle = NativeMethods.CreateWaitableTimer(IntPtr.Zero, true, "WaitableTimer"); + NativeMethods.SetWaitableTimer(handle, ref interval, 0, timerComplete, IntPtr.Zero, true); + return handle; + } + + private static async Task RunServer() + { + while (!CancellationToken.IsCancellationRequested) + { + await using NamedPipeServerStream pipeServer = + new NamedPipeServerStream("ffxiv_TheWatcher", PipeDirection.In, 1, PipeTransmissionMode.Message); + Console.WriteLine($"[{DateTime.Now}] Waiting for client..."); + + await pipeServer.WaitForConnectionAsync(); + Console.WriteLine($"[{DateTime.Now}] Client connected..."); + try + { + string message = await ReadMessage(pipeServer); + Console.WriteLine($"[{DateTime.Now}] Client message: {message}"); + + if (!EnableSleep) + { + Console.WriteLine($"[{DateTime.Now}] Ignoring sleep"); + } + else if (double.TryParse(message, out double duration)) + { + TimeSpan timeSpan = TimeSpan.FromSeconds(duration); + if (timeSpan >= TimeSpan.FromMinutes(10)) + { + DateTime sleep = DateTime.Now.Add(timeSpan).Add(TimeSpan.FromMinutes(-5)); + await Task.Delay(TimeSpan.FromSeconds(35)); + SleepUntil(sleep); + } + else + Console.WriteLine($"[{DateTime.Now}] Not enough sleep duration"); + } + } + catch (IOException e) + { + Console.WriteLine("ERROR: {0}", e.Message); + } + + Console.WriteLine($"[{DateTime.Now}] Finished client loop..."); + } + } + + + private static async Task ReadMessage(PipeStream pipe) + { + byte[] buffer = new byte[1024]; + using var ms = new MemoryStream(); + do + { + var readBytes = await pipe.ReadAsync(buffer, 0, buffer.Length); + await ms.WriteAsync(buffer, 0, readBytes); + } while (!pipe.IsMessageComplete); + + return Encoding.UTF8.GetString(ms.ToArray()); + } } diff --git a/TheWatcher/TheWatcher.csproj b/TheWatcher/TheWatcher.csproj index 44344c2..b2873ad 100644 --- a/TheWatcher/TheWatcher.csproj +++ b/TheWatcher/TheWatcher.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + windows