This commit is contained in:
Liza 2024-03-21 02:01:17 +01:00
parent f1373fcbfa
commit 24e5bafeb9
Signed by: liza
GPG Key ID: 7199F8D727D55F67
9 changed files with 252 additions and 42 deletions

@ -1 +1 @@
Subproject commit 75a0db11d7cc982b54f9cacb8a2b9c17b023b718 Subproject commit 6f0aaa55bce6ec79fd4d72f84f21597b39e5445d

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Version>0.1</Version> <Version>0.1</Version>
<LangVersion>11.0</LangVersion> <LangVersion>12</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@ -13,6 +13,7 @@
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap> <PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DebugType>portable</DebugType> <DebugType>portable</DebugType>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@ -1,74 +1,159 @@
using System; using System;
using System.Collections.Generic;
using System.IO.Pipes;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using AutoRetainerAPI; 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;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ECommons; using ECommons;
using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib; using LLib;
using LLib.GameUI;
using Task = System.Threading.Tasks.Task;
namespace AutoShutdown; namespace AutoShutdown;
public sealed class Plogon : IDalamudPlugin public sealed class Plogon : IDalamudPlugin
{ {
private readonly nint _skipMovieAddress;
private readonly byte[] _skipMovieOriginalBytes;
private readonly AutoRetainerApi _autoRetainerApi; private readonly AutoRetainerApi _autoRetainerApi;
private readonly DalamudReflector _reflector; private readonly DalamudReflector _reflector;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IGameGui _gameGui;
private readonly IFramework _framework;
public Plogon(DalamudPluginInterface pluginInterface, IFramework framework, IPluginLog pluginLog, 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); ECommonsMain.Init(pluginInterface, this);
_autoRetainerApi = new AutoRetainerApi(); _autoRetainerApi = new AutoRetainerApi();
_reflector = new DalamudReflector(pluginInterface, framework, pluginLog); _reflector = new DalamudReflector(pluginInterface, framework, pluginLog);
_framework = framework;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_clientState = clientState; _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 DateTime ShutdownAt { get; } = DateTime.Now.AddDays(2);
private long ShutdownAt => StartedAt + (long)TimeSpan.FromDays(2).TotalMilliseconds;
private void SetupShutdown() private unsafe void FrameworkUpdate(IFramework _)
{
if (DateTime.Now > ShutdownAt && _gameGui.TryGetAddonByName<AtkUnitBase>("_TitleMenu", out var addon))
{
Environment.Exit(0);
}
}
private bool IsMultiAndNightMode()
{ {
if (_autoRetainerApi.Ready && _reflector.TryGetDalamudPlugin("AutoRetainer", out var autoRetainer, false, true)) 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 config =
var forceShutdownAt = shutdown.GetField("ForceShutdownAt", BindingFlags.Static | BindingFlags.NonPublic)!; 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; //_pluginLog.Information($"AR: multi={multi}, night={nightMode}");
if (currentShutdownAt == 0)
{ return multi && nightMode;
_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)}");
}
} }
// move to background using alt+esc return false;
unsafe }
private void MoveToBackground()
{
if (IsMultiAndNightMode())
{ {
if (!Framework.Instance()->WindowInactive) // move to background using alt+esc
SendKeys.SendWait("%({esc})"); unsafe
{
if (!Framework.Instance()->WindowInactive)
SendKeys.SendWait("%({esc})");
}
} }
} }
private void OnLogout()
{
if (IsMultiAndNightMode())
{
List<TimeSpan> 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() public void Dispose()
{ {
_clientState.Logout -= SetupShutdown; SafeMemory.WriteBytes(_skipMovieAddress, _skipMovieOriginalBytes);
_framework.Update -= FrameworkUpdate;
_clientState.Logout -= OnLogout;
_reflector.Dispose(); _reflector.Dispose();
_autoRetainerApi.Dispose(); _autoRetainerApi.Dispose();
ECommonsMain.Dispose(); ECommonsMain.Dispose();

View File

@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net7.0-windows7.0": { "net8.0-windows7.0": {
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[2.1.12, )", "requested": "[2.1.12, )",

@ -1 +1 @@
Subproject commit f1c688a0599b41d70230021328a575da7351cf91 Subproject commit d238d4188e8b47b11252d75cb5e4b678b8da2756

2
LLib

@ -1 +1 @@
Subproject commit 865a6080319f8ccbcd5fd5b0004404822b6e60d4 Subproject commit 3792244261a9f5426a7916f5a6dd1966238ba84a

View File

@ -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);
}

View File

@ -1,25 +1,28 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Text;
namespace TheWatcher; namespace TheWatcher;
internal sealed class Program internal sealed class Program
{ {
private static CancellationToken CancellationToken { get; set; }
private static bool EnableSleep { get; set; }
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
Console.WriteLine("o.O"); Console.WriteLine("o.O");
using CancellationTokenSource cts = new CancellationTokenSource(); using CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken = cts.Token;
_ = Task.Factory.StartNew(async () => { await RunServer(); }, TaskCreationOptions.LongRunning);
Console.CancelKeyPress += (_, e) => Console.CancelKeyPress += (_, e) =>
{ {
try EnableSleep = !EnableSleep;
{ Console.WriteLine($"[{DateTime.Now}] Sleep => {EnableSleep}");
// ReSharper disable once AccessToDisposedClosure
cts.Cancel();
}
catch (ObjectDisposedException)
{
}
e.Cancel = true; 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<string> 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());
}
} }

View File

@ -5,6 +5,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<TargetPlatformIdentifier>windows</TargetPlatformIdentifier>
</PropertyGroup> </PropertyGroup>
</Project> </Project>