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

View File

@ -1,63 +1,104 @@
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<AtkUnitBase>("_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($"AR: multi={multi}, night={nightMode}");
return multi && nightMode;
}
return false;
}
private void MoveToBackground()
{
_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
if (IsMultiAndNightMode())
{
_pluginLog.Information(
$"Shutdown is already set to {new DateTime(TimeSpan.FromMilliseconds(currentShutdownAt).Ticks)}");
}
}
// move to background using alt+esc
unsafe
{
@ -65,10 +106,54 @@ public sealed class Plogon : IDalamudPlugin
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()
{
_clientState.Logout -= SetupShutdown;
SafeMemory.WriteBytes(_skipMovieAddress, _skipMovieOriginalBytes);
_framework.Update -= FrameworkUpdate;
_clientState.Logout -= OnLogout;
_reflector.Dispose();
_autoRetainerApi.Dispose();
ECommonsMain.Dispose();

View File

@ -1,7 +1,7 @@
{
"version": 1,
"dependencies": {
"net7.0-windows7.0": {
"net8.0-windows7.0": {
"DalamudPackager": {
"type": "Direct",
"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.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<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>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetPlatformIdentifier>windows</TargetPlatformIdentifier>
</PropertyGroup>
</Project>