Load services asynchronously whenever possible (#893)

This commit is contained in:
kizer 2022-06-25 05:12:51 +09:00 committed by GitHub
parent fba8c7163c
commit 8e7f370ddd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 959 additions and 899 deletions

View file

@ -21,6 +21,8 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::WaitMessageboxFlags
value = static_cast<DalamudStartInfo::WaitMessageboxFlags>(static_cast<int>(value) | static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize));
else if (item == "beforedalamudentrypoint")
value = static_cast<DalamudStartInfo::WaitMessageboxFlags>(static_cast<int>(value) | static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint));
else if (item == "beforedalamudconstruct")
value = static_cast<DalamudStartInfo::WaitMessageboxFlags>(static_cast<int>(value) | static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudConstruct));
}
}

View file

@ -5,6 +5,7 @@ struct DalamudStartInfo {
None = 0,
BeforeInitialize = 1 << 0,
BeforeDalamudEntrypoint = 1 << 1,
BeforeDalamudConstruct = 1 << 2,
};
friend void from_json(const nlohmann::json&, WaitMessageboxFlags&);

View file

@ -9,7 +9,7 @@
HMODULE g_hModule;
HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
DllExport DWORD WINAPI Initialize(LPVOID lpParam, HANDLE hMainThreadContinue) {
DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
g_startInfo.from_envvars();
std::string jsonParseError;
@ -149,11 +149,6 @@ DllExport DWORD WINAPI Initialize(LPVOID lpParam, HANDLE hMainThreadContinue) {
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))
MessageBoxW(nullptr, L"Press OK to continue (BeforeDalamudEntrypoint)", L"Dalamud Boot", MB_OK);
if (hMainThreadContinue) {
// Let the game initialize.
SetEvent(hMainThreadContinue);
}
// We don't need to do this anymore, Dalamud now loads without needing the window to be there. Speed!
// utils::wait_for_game_window();
@ -164,6 +159,10 @@ DllExport DWORD WINAPI Initialize(LPVOID lpParam, HANDLE hMainThreadContinue) {
return 0;
}
DllExport DWORD WINAPI Initialize(LPVOID lpParam) {
return InitializeImpl(lpParam, CreateEvent(nullptr, TRUE, FALSE, nullptr));
}
BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpReserved) {
DisableThreadLibraryCalls(hModule);

View file

@ -2,7 +2,7 @@
#include "logging.h"
DllExport DWORD WINAPI Initialize(LPVOID lpParam, HANDLE hMainThreadContinue);
DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
struct RewrittenEntryPointParameters {
void* pAllocation;
@ -368,7 +368,7 @@ DllExport void WINAPI RewrittenEntryPoint(RewrittenEntryPointParameters& params)
loadInfo = params.pLoadInfo;
}
Initialize(&loadInfo[0], params.hMainThreadContinue);
InitializeImpl(&loadInfo[0], params.hMainThreadContinue);
return 0;
} catch (const std::exception& e) {
MessageBoxA(nullptr, std::format("Failed to load Dalamud.\n\nError: {}", e.what()).c_str(), "Dalamud.Boot", MB_OK | MB_ICONERROR);
@ -380,6 +380,5 @@ DllExport void WINAPI RewrittenEntryPoint(RewrittenEntryPointParameters& params)
CloseHandle(params.hMainThread);
WaitForSingleObject(params.hMainThreadContinue, INFINITE);
CloseHandle(params.hMainThreadContinue);
VirtualFree(params.pAllocation, 0, MEM_RELEASE);
}

View file

@ -61,6 +61,7 @@
<ItemGroup>
<PackageReference Include="Iced" Version="1.13.0" />
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="PeNet" Version="2.6.4" />
<PackageReference Include="Reloaded.Memory" Version="4.1.1" />

View file

@ -83,8 +83,12 @@ namespace Dalamud.Injector
}
startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args);
args.Remove("--console"); // Remove "console" flag, already handled
args.Remove("--etw"); // Remove "etw" flag, already handled
// Remove already handled arguments
args.Remove("--console");
args.Remove("--msgbox1");
args.Remove("--msgbox2");
args.Remove("--msgbox3");
args.Remove("--etw");
var mainCommand = args[1].ToLowerInvariant();
if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand)
@ -311,7 +315,9 @@ namespace Dalamud.Injector
startInfo.BootLogPath = GetLogPath("dalamud.boot");
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess" };
startInfo.BootDotnetOpenProcessHookMode = 0;
// startInfo.BootWaitMessageBox = 2;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox3") ? 4 : 0;
// startInfo.BootUnhookDlls = new List<string>() { "kernel32.dll", "ntdll.dll", "user32.dll" };
return startInfo;
@ -338,6 +344,7 @@ namespace Dalamud.Injector
Console.WriteLine("{0} [-m entrypoint|inject] [--mode=entrypoint|inject]", exeSpaces);
Console.WriteLine("{0} [--handle-owner=inherited-handle-value]", exeSpaces);
Console.WriteLine("{0} [--without-dalamud] [--no-fix-acl]", exeSpaces);
Console.WriteLine("{0} [--no-wait]", exeSpaces);
Console.WriteLine("{0} [-- game_arg1=value1 game_arg2=value2 ...]", exeSpaces);
}
@ -349,6 +356,7 @@ namespace Dalamud.Injector
Console.WriteLine("Verbose logging:\t[-v]");
Console.WriteLine("Show Console:\t[--console]");
Console.WriteLine("Enable ETW:\t[--etw]");
Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2]");
return 0;
}
@ -464,6 +472,7 @@ namespace Dalamud.Injector
var handleOwner = IntPtr.Zero;
var withoutDalamud = false;
var noFixAcl = false;
var waitForGameWindow = true;
var parsingGameArgument = false;
for (var i = 2; i < args.Count; i++)
@ -480,6 +489,8 @@ namespace Dalamud.Injector
useFakeArguments = true;
else if (args[i] == "--without-dalamud")
withoutDalamud = true;
else if (args[i] == "--no-wait")
waitForGameWindow = false;
else if (args[i] == "--no-fix-acl" || args[i] == "--no-acl-fix")
noFixAcl = true;
else if (args[i] == "-g")
@ -596,7 +607,7 @@ namespace Dalamud.Injector
Log.Verbose("RewriteRemoteEntryPointW called!");
}
});
}, waitForGameWindow);
Log.Verbose("Game process started with PID {0}", process.Id);

View file

@ -235,6 +235,11 @@ namespace Dalamud.Configuration.Internal
/// </summary>
public bool IsAntiAntiDebugEnabled { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether to resume game main thread after plugins load.
/// </summary>
public bool IsResumeGameAfterPluginLoad { get; set; } = false;
/// <summary>
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
/// </summary>

View file

@ -4,7 +4,7 @@ using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
@ -49,7 +49,6 @@ namespace Dalamud
private readonly ManualResetEvent unloadSignal;
private readonly ManualResetEvent finishUnloadSignal;
private readonly IntPtr mainThreadContinueEvent;
private MonoMod.RuntimeDetour.Hook processMonoHook;
private bool hasDisposedPlugins = false;
@ -65,10 +64,6 @@ namespace Dalamud
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent)
{
Service<Dalamud>.Set(this);
Service<DalamudStartInfo>.Set(info);
Service<DalamudConfiguration>.Set(configuration);
this.LogLevelSwitch = loggingLevelSwitch;
this.unloadSignal = new ManualResetEvent(false);
@ -77,7 +72,55 @@ namespace Dalamud
this.finishUnloadSignal = finishSignal;
this.finishUnloadSignal.Reset();
this.mainThreadContinueEvent = mainThreadContinueEvent;
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
Service<Dalamud>.Provide(this);
Service<DalamudStartInfo>.Provide(info);
Service<DalamudConfiguration>.Provide(configuration);
if (!configuration.IsResumeGameAfterPluginLoad)
{
NativeFunctions.SetEvent(mainThreadContinueEvent);
_ = ServiceManager.InitializeEarlyLoadableServices();
}
else
{
Task.Run(async () =>
{
try
{
var tasks = new[]
{
ServiceManager.InitializeEarlyLoadableServices(),
ServiceManager.BlockingResolved,
};
await Task.WhenAny(tasks);
foreach (var task in tasks)
{
if (task.IsFaulted)
throw task.Exception!;
}
NativeFunctions.SetEvent(mainThreadContinueEvent);
await Task.WhenAll(tasks);
foreach (var task in tasks)
{
if (task.IsFaulted)
throw task.Exception!;
}
}
catch (Exception e)
{
Log.Error(e, "Service initialization failure");
}
finally
{
NativeFunctions.SetEvent(mainThreadContinueEvent);
}
});
}
}
/// <summary>
@ -88,262 +131,7 @@ namespace Dalamud
/// <summary>
/// Gets location of stored assets.
/// </summary>
internal DirectoryInfo AssetDirectory => new(Service<DalamudStartInfo>.Get().AssetDirectory);
/// <summary>
/// Runs tier 1 of the Dalamud initialization process.
/// </summary>
public void LoadTier1()
{
using var tier1Timing = Timings.Start("Tier 1 Init");
try
{
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
Service<ServiceContainer>.Set();
// Initialize the process information.
var info = Service<DalamudStartInfo>.Get();
var cacheDir = new DirectoryInfo(Path.Combine(info.WorkingDirectory!, "cachedSigs"));
if (!cacheDir.Exists)
cacheDir.Create();
Service<SigScanner>.Set(
new SigScanner(true, new FileInfo(Path.Combine(cacheDir.FullName, $"{info.GameVersion}.json"))));
Service<HookManager>.Set();
// Initialize FFXIVClientStructs function resolver
using (Timings.Start("CS Resolver Init"))
{
FFXIVClientStructs.Resolver.InitializeParallel(new FileInfo(Path.Combine(cacheDir.FullName, $"{info.GameVersion}_cs.json")));
Log.Information("[T1] FFXIVClientStructs initialized!");
}
// Initialize game subsystem
var framework = Service<Framework>.Set();
Log.Information("[T1] Framework OK!");
#if DEBUG
Service<TaskTracker>.Set();
Log.Information("[T1] TaskTracker OK!");
#endif
Service<GameNetwork>.Set();
Service<GameGui>.Set();
framework.Enable();
Log.Information("[T1] Load complete!");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 1 load failed");
this.Unload();
}
finally
{
// Signal the main game thread to continue
// TODO: This is done in rewrite_entrypoint.cpp again to avoid a race condition. Should be fixed!
// NativeFunctions.SetEvent(this.mainThreadContinueEvent);
// Timings.Event("Game kickoff");
// Log.Information("[T1] Game thread continued!");
}
}
/// <summary>
/// Runs tier 2 of the Dalamud initialization process.
/// </summary>
/// <returns>Whether or not the load succeeded.</returns>
public bool LoadTier2()
{
// This marks the first time we are actually on the game's main thread
ThreadSafety.MarkMainThread();
using var tier2Timing = Timings.Start("Tier 2 Init");
try
{
var configuration = Service<DalamudConfiguration>.Get();
var antiDebug = Service<AntiDebug>.Set();
if (!antiDebug.IsEnabled)
{
#if DEBUG
antiDebug.Enable();
#else
if (configuration.IsAntiAntiDebugEnabled)
antiDebug.Enable();
#endif
}
Log.Information("[T2] AntiDebug OK!");
Service<WinSockHandlers>.Set();
Log.Information("[T2] WinSock OK!");
Service<NetworkHandlers>.Set();
Log.Information("[T2] NH OK!");
using (Timings.Start("DM Init"))
{
try
{
Service<DataManager>.Set().Initialize(this.AssetDirectory.FullName);
}
catch (Exception e)
{
Log.Error(e, "Could not initialize DataManager");
this.Unload();
return false;
}
}
Log.Information("[T2] Data OK!");
using (Timings.Start("CS Init"))
{
Service<ClientState>.Set();
Log.Information("[T2] CS OK!");
}
var localization = Service<Localization>.Set(new Localization(Path.Combine(this.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_"));
if (!string.IsNullOrEmpty(configuration.LanguageOverride))
{
localization.SetupWithLangCode(configuration.LanguageOverride);
}
else
{
localization.SetupWithUiCulture();
}
Log.Information("[T2] LOC OK!");
// This is enabled in ImGuiScene setup
using (Timings.Start("IME Init"))
{
Service<DalamudIME>.Set();
Log.Information("[T2] IME OK!");
}
using (Timings.Start("IM Enable"))
{
Service<InterfaceManager>.Set().Enable();
Log.Information("[T2] IM OK!");
}
using (Timings.Start("GFM Init"))
{
Service<GameFontManager>.Set();
Log.Information("[T2] GFM OK!");
}
#pragma warning disable CS0618 // Type or member is obsolete
Service<SeStringManager>.Set();
#pragma warning restore CS0618 // Type or member is obsolete
Log.Information("[T2] SeString OK!");
using (Timings.Start("CM Init"))
{
Service<CommandManager>.Set();
Service<DalamudCommands>.Set().SetupCommands();
Log.Information("[T2] CM OK!");
}
Service<ChatHandlers>.Set();
Log.Information("[T2] CH OK!");
using (Timings.Start("CS Enable"))
{
Service<ClientState>.Get().Enable();
Log.Information("[T2] CS ENABLE!");
}
Service<DalamudAtkTweaks>.Set().Enable();
Log.Information("[T2] ATKTWEAKS ENABLE!");
Log.Information("[T2] Load complete!");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 2 load failed");
this.Unload();
return false;
}
return true;
}
/// <summary>
/// Runs tier 3 of the Dalamud initialization process.
/// </summary>
/// <returns>Whether or not the load succeeded.</returns>
public bool LoadTier3()
{
using var tier3Timing = Timings.Start("Tier 3 Init");
ThreadSafety.AssertMainThread();
try
{
Log.Information("[T3] START!");
Service<TitleScreenMenu>.Set();
PluginManager pluginManager;
using (Timings.Start("PM Init"))
{
pluginManager = Service<PluginManager>.Set();
Service<CallGate>.Set();
Log.Information("[T3] PM OK!");
}
Service<DalamudInterface>.Set();
Log.Information("[T3] DUI OK!");
try
{
using (Timings.Start("PM Load Plugin Repos"))
{
_ = pluginManager.SetPluginReposFromConfigAsync(false);
pluginManager.OnInstalledPluginsChanged += Troubleshooting.LogTroubleshooting;
Log.Information("[T3] PM repos OK!");
}
using (Timings.Start("PM Cleanup Plugins"))
{
pluginManager.CleanupPlugins();
Log.Information("[T3] PMC OK!");
}
using (Timings.Start("PM Load Sync Plugins"))
{
pluginManager.LoadAllPlugins();
Log.Information("[T3] PML OK!");
}
}
catch (Exception ex)
{
Log.Error(ex, "Plugin load failed");
}
Troubleshooting.LogTroubleshooting();
Log.Information("Dalamud is ready");
Timings.Event("Dalamud ready");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 3 load failed");
this.Unload();
return false;
}
return true;
}
internal DirectoryInfo AssetDirectory => new(Service<DalamudStartInfo>.Get().AssetDirectory!);
/// <summary>
/// Queue an unload of Dalamud when it gets the chance.

View file

@ -86,12 +86,12 @@ namespace Dalamud
/// <summary>
/// Gets or sets a value that specifies how much to wait before a new Dalamud session.
/// </summary>
public int DelayInitializeMs { get; set; } = 0;
public int DelayInitializeMs { get; set; }
/// <summary>
/// Gets or sets the path the boot log file is supposed to be written to.
/// </summary>
public string BootLogPath { get; set; }
public string? BootLogPath { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a Boot console should be shown.

View file

@ -26,22 +26,91 @@ namespace Dalamud.Data
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class DataManager : IDisposable
{
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private Thread luminaResourceThread;
private CancellationTokenSource luminaCancellationTokenSource;
private readonly Thread luminaResourceThread;
private readonly CancellationTokenSource luminaCancellationTokenSource;
/// <summary>
/// Initializes a new instance of the <see cref="DataManager"/> class.
/// </summary>
internal DataManager()
[ServiceManager.ServiceConstructor]
private DataManager(DalamudStartInfo dalamudStartInfo, Dalamud dalamud)
{
this.Language = Service<DalamudStartInfo>.Get().Language;
this.Language = dalamudStartInfo.Language;
// Set up default values so plugins do not null-reference when data is being loaded.
this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(new Dictionary<string, ushort>());
var baseDir = dalamud.AssetDirectory.FullName;
try
{
Log.Verbose("Starting data load...");
var zoneOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")))!;
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(zoneOpCodeDict);
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
var clientOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")))!;
this.ClientOpCodes = new ReadOnlyDictionary<string, ushort>(clientOpCodeDict);
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
using (Timings.Start("Lumina Init"))
{
var luminaOptions = new LuminaOptions
{
LoadMultithreaded = true,
CacheFileResources = true,
#if DEBUG
PanicOnSheetChecksumMismatch = true,
#else
PanicOnSheetChecksumMismatch = false,
#endif
DefaultExcelLanguage = this.Language.ToLumina(),
};
var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null)
{
this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName)!, "sqpack"), luminaOptions);
}
else
{
throw new Exception("Could not main module.");
}
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
}
this.IsDataReady = true;
this.luminaCancellationTokenSource = new();
var luminaCancellationToken = this.luminaCancellationTokenSource.Token;
this.luminaResourceThread = new(() =>
{
while (!luminaCancellationToken.IsCancellationRequested)
{
if (this.GameData.FileHandleManager.HasPendingFileLoads)
{
this.GameData.ProcessFileHandleQueue();
}
else
{
Thread.Sleep(5);
}
}
});
this.luminaResourceThread.Start();
}
catch (Exception ex)
{
Log.Error(ex, "Could not download data.");
}
}
/// <summary>
@ -68,7 +137,7 @@ namespace Dalamud.Data
/// <summary>
/// Gets an <see cref="ExcelModule"/> object which gives access to any of the game's sheet data.
/// </summary>
public ExcelModule Excel => this.GameData?.Excel;
public ExcelModule Excel => this.GameData.Excel;
/// <summary>
/// Gets a value indicating whether Game Data is ready to be read.
@ -180,7 +249,7 @@ namespace Dalamud.Data
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(string type, uint iconId)
public TexFile? GetIcon(string? type, uint iconId)
{
type ??= string.Empty;
if (type.Length > 0 && !type.EndsWith("/"))
@ -276,81 +345,5 @@ namespace Dalamud.Data
{
this.luminaCancellationTokenSource.Cancel();
}
/// <summary>
/// Initialize this data manager.
/// </summary>
/// <param name="baseDir">The directory to load data from.</param>
internal void Initialize(string baseDir)
{
try
{
Log.Verbose("Starting data load...");
var zoneOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")));
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(zoneOpCodeDict);
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
var clientOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")));
this.ClientOpCodes = new ReadOnlyDictionary<string, ushort>(clientOpCodeDict);
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
using (Timings.Start("Lumina Init"))
{
var luminaOptions = new LuminaOptions
{
LoadMultithreaded = true,
CacheFileResources = true,
#if DEBUG
PanicOnSheetChecksumMismatch = true,
#else
PanicOnSheetChecksumMismatch = false,
#endif
DefaultExcelLanguage = this.Language.ToLumina(),
};
var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null)
{
this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName), "sqpack"), luminaOptions);
}
else
{
throw new Exception("Could not main module.");
}
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
}
this.IsDataReady = true;
this.luminaCancellationTokenSource = new();
var luminaCancellationToken = this.luminaCancellationTokenSource.Token;
this.luminaResourceThread = new(() =>
{
while (!luminaCancellationToken.IsCancellationRequested)
{
if (this.GameData.FileHandleManager.HasPendingFileLoads)
{
this.GameData.ProcessFileHandleQueue();
}
else
{
Thread.Sleep(5);
}
}
});
this.luminaResourceThread.Start();
}
catch (Exception ex)
{
Log.Error(ex, "Could not download data.");
}
}
}
}

View file

@ -47,8 +47,11 @@ namespace Dalamud
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public static void Initialize(IntPtr infoPtr, IntPtr mainThreadContinueEvent)
{
var infoStr = Marshal.PtrToStringUTF8(infoPtr);
var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr);
var infoStr = Marshal.PtrToStringUTF8(infoPtr)!;
var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr)!;
if ((info.BootWaitMessageBox & 4) != 0)
MessageBoxW(IntPtr.Zero, "Press OK to continue (BeforeDalamudConstruct)", "Dalamud Boot", MessageBoxType.Ok);
new Thread(() => RunThread(info, mainThreadContinueEvent)).Start();
}
@ -148,9 +151,6 @@ namespace Dalamud
var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash}", Util.GetGitHash(), Util.GetGitHashClientStructs());
// Run session
dalamud.LoadTier1();
dalamud.WaitForUnload();
dalamud.Dispose();

View file

@ -20,15 +20,6 @@ namespace Dalamud.Game
/// </summary>
protected bool IsResolved { get; set; }
/// <summary>
/// Setup the resolver, calling the appopriate method based on the process architecture.
/// </summary>
public void Setup()
{
var scanner = Service<SigScanner>.Get();
this.Setup(scanner);
}
/// <summary>
/// Setup the resolver, calling the appopriate method based on the process architecture.
/// </summary>
@ -55,11 +46,13 @@ namespace Dalamud.Game
this.SetupInternal(scanner);
var className = this.GetType().Name;
DebugScannedValues[className] = new List<(string, IntPtr)>();
var list = new List<(string, IntPtr)>();
lock (DebugScannedValues)
DebugScannedValues[className] = list;
foreach (var property in this.GetType().GetProperties().Where(x => x.PropertyType == typeof(IntPtr)))
{
DebugScannedValues[className].Add((property.Name, (IntPtr)property.GetValue(this)));
list.Add((property.Name, (IntPtr)property.GetValue(this)));
}
this.IsResolved = true;

View file

@ -28,6 +28,7 @@ namespace Dalamud.Game
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public class ChatHandlers
{
// private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new()
@ -109,13 +110,9 @@ namespace Dalamud.Game
private bool hasSeenLoadingMsg;
private bool hasAutoUpdatedPlugins;
/// <summary>
/// Initializes a new instance of the <see cref="ChatHandlers"/> class.
/// </summary>
internal ChatHandlers()
[ServiceManager.ServiceConstructor]
private ChatHandlers(ChatGui chatGui)
{
var chatGui = Service<ChatGui>.Get();
chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
chatGui.ChatMessage += this.OnChatMessage;

View file

@ -14,18 +14,18 @@ namespace Dalamud.Game.ClientState.Aetherytes
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class AetheryteList
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address;
private readonly UpdateAetheryteListDelegate updateAetheryteListFunc;
/// <summary>
/// Initializes a new instance of the <see cref="AetheryteList"/> class.
/// </summary>
/// <param name="addressResolver">Client state address resolver.</param>
internal AetheryteList(ClientStateAddressResolver addressResolver)
[ServiceManager.ServiceConstructor]
private AetheryteList()
{
this.address = addressResolver;
this.address = this.clientState.AddressResolver;
this.updateAetheryteListFunc = Marshal.GetDelegateForFunctionPointer<UpdateAetheryteListDelegate>(this.address.UpdateAetheryteList);
Log.Verbose($"Teleport address 0x{this.address.Telepo.ToInt64():X}");
@ -40,9 +40,7 @@ namespace Dalamud.Game.ClientState.Aetherytes
{
get
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalPlayer == null)
if (this.clientState.LocalPlayer == null)
return 0;
this.Update();
@ -70,9 +68,7 @@ namespace Dalamud.Game.ClientState.Aetherytes
return null;
}
var clientState = Service<ClientState>.Get();
if (clientState.LocalPlayer == null)
if (this.clientState.LocalPlayer == null)
return null;
return new AetheryteEntry(TelepoStruct->TeleportList.Get((ulong)index));
@ -81,10 +77,8 @@ namespace Dalamud.Game.ClientState.Aetherytes
private void Update()
{
var clientState = Service<ClientState>.Get();
// this is very very important as otherwise it crashes
if (clientState.LocalPlayer == null)
if (this.clientState.LocalPlayer == null)
return;
this.updateAetheryteListFunc(this.address.Telepo, 0);

View file

@ -15,19 +15,17 @@ namespace Dalamud.Game.ClientState.Buddy
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class BuddyList
{
private const uint InvalidObjectID = 0xE0000000;
private readonly ClientStateAddressResolver address;
/// <summary>
/// Initializes a new instance of the <see cref="BuddyList"/> class.
/// </summary>
/// <param name="addressResolver">Client state address resolver.</param>
internal BuddyList(ClientStateAddressResolver addressResolver)
[ServiceManager.ServiceConstructor]
private BuddyList(ClientState clientState)
{
this.address = addressResolver;
this.address = clientState.AddressResolver;
Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}");
}

View file

@ -27,6 +27,7 @@ namespace Dalamud.Game.ClientState
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class ClientState : IDisposable
{
private readonly ClientStateAddressResolver address;
@ -36,46 +37,26 @@ namespace Dalamud.Game.ClientState
private bool lastFramePvP = false;
/// <summary>
/// Initializes a new instance of the <see cref="ClientState"/> class.
/// Set up client state access.
/// Gets client state address resolver.
/// </summary>
internal ClientState()
internal ClientStateAddressResolver AddressResolver => this.address;
[ServiceManager.ServiceConstructor]
private ClientState(Framework framework, NetworkHandlers networkHandlers, SigScanner sigScanner, DalamudStartInfo startInfo)
{
this.address = new ClientStateAddressResolver();
this.address.Setup();
this.address.Setup(sigScanner);
Log.Verbose("===== C L I E N T S T A T E =====");
this.ClientLanguage = Service<DalamudStartInfo>.Get().Language;
Service<ObjectTable>.Set(this.address);
Service<FateTable>.Set(this.address);
Service<PartyList>.Set(this.address);
Service<BuddyList>.Set(this.address);
Service<JobGauges>.Set(this.address);
Service<KeyState>.Set(this.address);
Service<GamepadState>.Set(this.address);
Service<Conditions.Condition>.Set(this.address);
Service<TargetManager>.Set(this.address);
Service<AetheryteList>.Set(this.address);
this.ClientLanguage = startInfo.Language;
Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
this.setupTerritoryTypeHook = new Hook<SetupTerritoryTypeDelegate>(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
var framework = Service<Framework>.Get();
framework.Update += this.FrameworkOnOnUpdateEvent;
var networkHandlers = Service<NetworkHandlers>.Get();
networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
}

View file

@ -11,6 +11,7 @@ namespace Dalamud.Game.ClientState.Conditions
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class Condition
{
/// <summary>
@ -20,12 +21,10 @@ namespace Dalamud.Game.ClientState.Conditions
private readonly bool[] cache = new bool[MaxConditionEntries];
/// <summary>
/// Initializes a new instance of the <see cref="Condition"/> class.
/// </summary>
/// <param name="resolver">The ClientStateAddressResolver instance.</param>
internal Condition(ClientStateAddressResolver resolver)
[ServiceManager.ServiceConstructor]
private Condition(ClientState clientState)
{
var resolver = clientState.AddressResolver;
this.Address = resolver.ConditionFlags;
}

View file

@ -13,17 +13,15 @@ namespace Dalamud.Game.ClientState.Fates
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class FateTable
{
private readonly ClientStateAddressResolver address;
/// <summary>
/// Initializes a new instance of the <see cref="FateTable"/> class.
/// </summary>
/// <param name="addressResolver">Client state address resolver.</param>
internal FateTable(ClientStateAddressResolver addressResolver)
[ServiceManager.ServiceConstructor]
private FateTable(ClientState clientState)
{
this.address = addressResolver;
this.address = clientState.AddressResolver;
Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}");
}

View file

@ -15,6 +15,7 @@ namespace Dalamud.Game.ClientState.GamePad
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0.0")]
[ServiceManager.BlockingEarlyLoadedService]
public unsafe class GamepadState : IDisposable
{
private readonly Hook<ControllerPoll> gamepadPoll;
@ -26,12 +27,10 @@ namespace Dalamud.Game.ClientState.GamePad
private int rightStickX;
private int rightStickY;
/// <summary>
/// Initializes a new instance of the <see cref="GamepadState" /> class.
/// </summary>
/// <param name="resolver">Resolver knowing the pointer to the GamepadPoll function.</param>
public GamepadState(ClientStateAddressResolver resolver)
[ServiceManager.ServiceConstructor]
private GamepadState(ClientState clientState)
{
var resolver = clientState.AddressResolver;
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
this.gamepadPoll = new Hook<ControllerPoll>(resolver.GamepadPoll, this.GamepadPollDetour);
}

View file

@ -14,17 +14,15 @@ namespace Dalamud.Game.ClientState.JobGauge
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public class JobGauges
{
private Dictionary<Type, JobGaugeBase> cache = new();
/// <summary>
/// Initializes a new instance of the <see cref="JobGauges"/> class.
/// </summary>
/// <param name="addressResolver">Address resolver with the JobGauge memory location(s).</param>
public JobGauges(ClientStateAddressResolver addressResolver)
[ServiceManager.ServiceConstructor]
private JobGauges(ClientState clientState)
{
this.Address = addressResolver.JobGaugeData;
this.Address = clientState.AddressResolver.JobGaugeData;
Log.Verbose($"JobGaugeData address 0x{this.Address.ToInt64():X}");
}

View file

@ -22,6 +22,7 @@ namespace Dalamud.Game.ClientState.Keys
/// </remarks>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public class KeyState
{
// The array is accessed in a way that this limit doesn't appear to exist
@ -32,14 +33,11 @@ namespace Dalamud.Game.ClientState.Keys
private readonly IntPtr indexBase;
private VirtualKey[] validVirtualKeyCache = null;
/// <summary>
/// Initializes a new instance of the <see cref="KeyState"/> class.
/// </summary>
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
public KeyState(ClientStateAddressResolver addressResolver)
[ServiceManager.ServiceConstructor]
private KeyState(SigScanner sigScanner, ClientState clientState)
{
var moduleBaseAddress = Service<SigScanner>.Get().Module.BaseAddress;
var moduleBaseAddress = sigScanner.Module.BaseAddress;
var addressResolver = clientState.AddressResolver;
this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState);
this.indexBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardStateIndexArray);

View file

@ -16,19 +16,17 @@ namespace Dalamud.Game.ClientState.Objects
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class ObjectTable
{
private const int ObjectTableLength = 424;
private readonly ClientStateAddressResolver address;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectTable"/> class.
/// </summary>
/// <param name="addressResolver">Client state address resolver.</param>
internal ObjectTable(ClientStateAddressResolver addressResolver)
[ServiceManager.ServiceConstructor]
private ObjectTable(ClientState clientState)
{
this.address = addressResolver;
this.address = clientState.AddressResolver;
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
}

View file

@ -11,17 +11,15 @@ namespace Dalamud.Game.ClientState.Objects
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe class TargetManager
{
private readonly ClientStateAddressResolver address;
/// <summary>
/// Initializes a new instance of the <see cref="TargetManager"/> class.
/// </summary>
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
internal TargetManager(ClientStateAddressResolver addressResolver)
[ServiceManager.ServiceConstructor]
private TargetManager(ClientState clientState)
{
this.address = addressResolver;
this.address = clientState.AddressResolver;
}
/// <summary>

View file

@ -14,6 +14,7 @@ namespace Dalamud.Game.ClientState.Party
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe partial class PartyList
{
private const int GroupLength = 8;
@ -21,13 +22,10 @@ namespace Dalamud.Game.ClientState.Party
private readonly ClientStateAddressResolver address;
/// <summary>
/// Initializes a new instance of the <see cref="PartyList"/> class.
/// </summary>
/// <param name="addressResolver">Client state address resolver.</param>
internal PartyList(ClientStateAddressResolver addressResolver)
[ServiceManager.ServiceConstructor]
private PartyList(ClientState clientState)
{
this.address = addressResolver;
this.address = clientState.AddressResolver;
Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}");
}

View file

@ -17,6 +17,7 @@ namespace Dalamud.Game.Command
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class CommandManager
{
private readonly Dictionary<string, CommandInfo> commandMap = new();
@ -27,13 +28,9 @@ namespace Dalamud.Game.Command
private readonly Regex commandRegexCn = new(@"^^(“|「)(?<command>.+)(”|」)(出现问题:该命令不存在|出現問題:該命令不存在)。$", RegexOptions.Compiled);
private readonly Regex currentLangCommandRegex;
/// <summary>
/// Initializes a new instance of the <see cref="CommandManager"/> class.
/// </summary>
internal CommandManager()
[ServiceManager.ServiceConstructor]
private CommandManager(DalamudStartInfo startInfo)
{
var startInfo = Service<DalamudStartInfo>.Get();
this.currentLangCommandRegex = startInfo.Language switch
{
ClientLanguage.Japanese => this.commandRegexJp,

View file

@ -24,6 +24,7 @@ namespace Dalamud.Game
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class Framework : IDisposable
{
private static Stopwatch statsStopwatch = new();
@ -31,27 +32,28 @@ namespace Dalamud.Game
private readonly List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
private readonly Stopwatch updateStopwatch = new();
private bool tier2Initialized = false;
private bool tier3Initialized = false;
private bool tierInitError = false;
private Hook<OnUpdateDetour> updateHook;
private Hook<OnDestroyDetour> freeHook;
private Hook<OnRealDestroyDelegate> destroyHook;
private Thread? frameworkUpdateThread;
/// <summary>
/// Initializes a new instance of the <see cref="Framework"/> class.
/// </summary>
internal Framework()
[ServiceManager.ServiceConstructor]
private Framework(GameGui gameGui, GameNetwork gameNetwork, SigScanner sigScanner)
{
this.Address = new FrameworkAddressResolver();
this.Address.Setup();
this.Address.Setup(sigScanner);
this.updateHook = new Hook<OnUpdateDetour>(this.Address.TickAddress, this.HandleFrameworkUpdate);
this.freeHook = new Hook<OnDestroyDetour>(this.Address.FreeAddress, this.HandleFrameworkFree);
this.destroyHook = new Hook<OnRealDestroyDelegate>(this.Address.DestroyAddress, this.HandleFrameworkDestroy);
gameGui.Enable();
gameNetwork.Enable();
this.updateHook.Enable();
this.freeHook.Enable();
this.destroyHook.Enable();
}
/// <summary>
@ -123,20 +125,6 @@ namespace Dalamud.Game
/// </summary>
internal bool DispatchUpdateEvents { get; set; } = true;
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
{
Service<LibcFunction>.Set();
Service<GameGui>.Get().Enable();
Service<GameNetwork>.Get().Enable();
this.updateHook.Enable();
this.freeHook.Enable();
this.destroyHook.Enable();
}
/// <summary>
/// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call.
/// </summary>
@ -238,39 +226,21 @@ namespace Dalamud.Game
private bool HandleFrameworkUpdate(IntPtr framework)
{
// If any of the tier loads failed, just go to the original code.
if (this.tierInitError)
goto original;
this.frameworkUpdateThread ??= Thread.CurrentThread;
var dalamud = Service<Dalamud>.Get();
// If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously
if (!this.tier2Initialized)
{
this.tier2Initialized = dalamud.LoadTier2();
if (!this.tier2Initialized)
this.tierInitError = true;
goto original;
}
// Plugins expect the interface to be available and ready, so we need to wait with plugins until we have init'd ImGui
if (!this.tier3Initialized && Service<InterfaceManager>.GetNullable()?.IsReady == true)
{
this.tier3Initialized = dalamud.LoadTier3();
if (!this.tier3Initialized)
this.tierInitError = true;
goto original;
}
ThreadSafety.MarkMainThread();
try
{
Service<ChatGui>.Get().UpdateQueue();
Service<ToastGui>.Get().UpdateQueue();
Service<GameNetwork>.Get().UpdateQueue();
var chatGui = Service<ChatGui>.GetNullable();
var toastGui = Service<ToastGui>.GetNullable();
var gameNetwork = Service<GameNetwork>.GetNullable();
if (chatGui == null || toastGui == null || gameNetwork == null)
goto original;
chatGui.UpdateQueue();
toastGui.UpdateQueue();
gameNetwork.UpdateQueue();
}
catch (Exception ex)
{

View file

@ -21,6 +21,7 @@ namespace Dalamud.Game.Gui
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class ChatGui : IDisposable
{
private readonly ChatGuiAddressResolver address;
@ -34,14 +35,11 @@ namespace Dalamud.Game.Gui
private IntPtr baseAddress = IntPtr.Zero;
/// <summary>
/// Initializes a new instance of the <see cref="ChatGui"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the ChatManager.</param>
internal ChatGui()
[ServiceManager.ServiceConstructor]
private ChatGui(SigScanner sigScanner)
{
this.address = new ChatGuiAddressResolver();
this.address.Setup();
this.address.Setup(sigScanner);
this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);

View file

@ -24,6 +24,7 @@ namespace Dalamud.Game.Gui.ContextMenus
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class ContextMenu : IDisposable
{
private const int MaxContextMenuItemsPerContextMenu = 32;
@ -47,13 +48,11 @@ namespace Dalamud.Game.Gui.ContextMenus
private OpenSubContextMenuItem? selectedOpenSubContextMenuItem;
private ContextMenuOpenedArgs? currentContextMenuOpenedArgs;
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
/// </summary>
public ContextMenu()
[ServiceManager.ServiceConstructor]
private ContextMenu(SigScanner sigScanner)
{
this.Address = new ContextMenuAddressResolver();
this.Address.Setup();
this.Address.Setup(sigScanner);
unsafe
{

View file

@ -17,6 +17,7 @@ namespace Dalamud.Game.Gui.Dtr
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe class DtrBar : IDisposable
{
private const uint BaseNodeId = 1000;
@ -24,13 +25,10 @@ namespace Dalamud.Game.Gui.Dtr
private List<DtrBarEntry> entries = new();
private uint runningNodeIds = BaseNodeId;
/// <summary>
/// Initializes a new instance of the <see cref="DtrBar"/> class.
/// </summary>
internal DtrBar()
[ServiceManager.ServiceConstructor]
private DtrBar(DalamudConfiguration configuration)
{
Service<Framework>.Get().Update += this.Update;
var configuration = Service<DalamudConfiguration>.Get();
configuration.DtrOrder ??= new List<string>();
configuration.DtrIgnore ??= new List<string>();

View file

@ -16,6 +16,7 @@ namespace Dalamud.Game.Gui.FlyText
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class FlyTextGui : IDisposable
{
/// <summary>
@ -28,13 +29,11 @@ namespace Dalamud.Game.Gui.FlyText
/// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
/// <summary>
/// Initializes a new instance of the <see cref="FlyTextGui"/> class.
/// </summary>
internal FlyTextGui()
[ServiceManager.ServiceConstructor]
private FlyTextGui(SigScanner sigScanner)
{
this.Address = new FlyTextGuiAddressResolver();
this.Address.Setup();
this.Address.Setup(sigScanner);
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = new Hook<CreateFlyTextDelegate>(this.Address.CreateFlyText, this.CreateFlyTextDetour);

View file

@ -27,6 +27,7 @@ namespace Dalamud.Game.Gui
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe class GameGui : IDisposable
{
private readonly GameGuiAddressResolver address;
@ -46,14 +47,11 @@ namespace Dalamud.Game.Gui
private GetUIMapObjectDelegate getUIMapObject;
private OpenMapWithFlagDelegate openMapWithFlag;
/// <summary>
/// Initializes a new instance of the <see cref="GameGui"/> class.
/// This class is responsible for many aspects of interacting with the native game UI.
/// </summary>
internal GameGui()
[ServiceManager.ServiceConstructor]
private GameGui(SigScanner sigScanner)
{
this.address = new GameGuiAddressResolver();
this.address.Setup();
this.address.Setup(sigScanner);
Log.Verbose("===== G A M E G U I =====");
Log.Verbose($"GameGuiManager address 0x{this.address.BaseAddress.ToInt64():X}");
@ -62,13 +60,6 @@ namespace Dalamud.Game.Gui
Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}");
Log.Verbose($"HandleImm address 0x{this.address.HandleImm.ToInt64():X}");
Service<ChatGui>.Set();
Service<PartyFinderGui>.Set();
Service<ToastGui>.Set();
Service<FlyTextGui>.Set();
Service<ContextMenu>.Set();
Service<DtrBar>.Set();
this.setGlobalBgmHook = new Hook<SetGlobalBgmDelegate>(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour);
this.handleItemHoverHook = new Hook<HandleItemHoverDelegate>(this.address.HandleItemHover, this.HandleItemHoverDetour);

View file

@ -18,6 +18,7 @@ namespace Dalamud.Game.Gui.Internal
/// <summary>
/// This class handles IME for non-English users.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class DalamudIME : IDisposable
{
private static readonly ModuleLog Log = new("IME");
@ -32,7 +33,9 @@ namespace Dalamud.Game.Gui.Internal
/// <summary>
/// Initializes a new instance of the <see cref="DalamudIME"/> class.
/// </summary>
internal DalamudIME()
/// <param name="tag">Tag.</param>
[ServiceManager.ServiceConstructor]
private DalamudIME()
{
}

View file

@ -15,6 +15,7 @@ namespace Dalamud.Game.Gui.PartyFinder
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class PartyFinderGui : IDisposable
{
private readonly PartyFinderAddressResolver address;
@ -25,10 +26,12 @@ namespace Dalamud.Game.Gui.PartyFinder
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderGui"/> class.
/// </summary>
internal PartyFinderGui()
/// <param name="tag">Tag.</param>
[ServiceManager.ServiceConstructor]
private PartyFinderGui(SigScanner sigScanner)
{
this.address = new PartyFinderAddressResolver();
this.address.Setup();
this.address.Setup(sigScanner);
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);

View file

@ -14,6 +14,7 @@ namespace Dalamud.Game.Gui.Toast
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class ToastGui : IDisposable
{
private const uint QuestToastCheckmarkMagic = 60081;
@ -31,10 +32,12 @@ namespace Dalamud.Game.Gui.Toast
/// <summary>
/// Initializes a new instance of the <see cref="ToastGui"/> class.
/// </summary>
internal ToastGui()
/// <param name="tag">Tag.</param>
[ServiceManager.ServiceConstructor]
private ToastGui(SigScanner sigScanner)
{
this.address = new ToastGuiAddressResolver();
this.address.Setup();
this.address.Setup(sigScanner);
this.showNormalToastHook = new Hook<ShowNormalToastDelegate>(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour));
this.showQuestToastHook = new Hook<ShowQuestToastDelegate>(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour));

View file

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Dalamud.Configuration.Internal;
using Serilog;
namespace Dalamud.Game.Internal
@ -8,22 +8,19 @@ namespace Dalamud.Game.Internal
/// <summary>
/// This class disables anti-debug functionality in the game client.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed partial class AntiDebug
{
private readonly byte[] nop = new byte[] { 0x31, 0xC0, 0x90, 0x90, 0x90, 0x90 };
private byte[] original;
private IntPtr debugCheckAddress;
/// <summary>
/// Initializes a new instance of the <see cref="AntiDebug"/> class.
/// </summary>
public AntiDebug()
[ServiceManager.ServiceConstructor]
private AntiDebug(SigScanner sigScanner)
{
var scanner = Service<SigScanner>.Get();
try
{
this.debugCheckAddress = scanner.ScanText("FF 15 ?? ?? ?? ?? 85 C0 74 11 41");
this.debugCheckAddress = sigScanner.ScanText("FF 15 ?? ?? ?? ?? 85 C0 74 11 41");
}
catch (KeyNotFoundException)
{
@ -31,6 +28,16 @@ namespace Dalamud.Game.Internal
}
Log.Verbose($"Debug check address 0x{this.debugCheckAddress.ToInt64():X}");
if (!this.IsEnabled)
{
#if DEBUG
this.Enable();
#else
if (Service<DalamudConfiguration>.Get().IsAntiAntiDebugEnabled)
this.Enable();
#endif
}
}
/// <summary>

View file

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Runtime.InteropServices;
using Dalamud.Game.Internal.DXGI.Definitions;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Serilog;
namespace Dalamud.Game.Internal.DXGI
@ -30,9 +31,28 @@ namespace Dalamud.Game.Internal.DXGI
/// <inheritdoc/>
protected override unsafe void Setup64Bit(SigScanner sig)
{
var kernelDev = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Device.Instance();
Device* kernelDev;
SwapChain* swapChain;
void* dxgiSwapChain;
var scVtbl = GetVTblAddresses(new IntPtr(kernelDev->SwapChain->DXGISwapChain), Enum.GetValues(typeof(IDXGISwapChainVtbl)).Length);
while (true)
{
kernelDev = Device.Instance();
if (kernelDev == null)
continue;
swapChain = kernelDev->SwapChain;
if (swapChain == null)
continue;
dxgiSwapChain = swapChain->DXGISwapChain;
if (dxgiSwapChain == null)
continue;
break;
}
var scVtbl = GetVTblAddresses(new IntPtr(dxgiSwapChain), Enum.GetValues(typeof(IDXGISwapChainVtbl)).Length);
this.Present = scVtbl[(int)IDXGISwapChainVtbl.Present];

View file

@ -23,6 +23,7 @@ namespace Dalamud.Game.Internal
/// <summary>
/// This class implements in-game Dalamud options in the in-game System menu.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe partial class DalamudAtkTweaks
{
private readonly AtkValueChangeType atkValueChangeType;
@ -37,13 +38,9 @@ namespace Dalamud.Game.Internal
private readonly string locDalamudPlugins;
private readonly string locDalamudSettings;
/// <summary>
/// Initializes a new instance of the <see cref="DalamudAtkTweaks"/> class.
/// </summary>
public DalamudAtkTweaks()
[ServiceManager.ServiceConstructor]
private DalamudAtkTweaks(SigScanner sigScanner)
{
var sigScanner = Service<SigScanner>.Get();
var openSystemMenuAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 32 C0 4C 8B AC 24 ?? ?? ?? ?? 48 8B 8D ?? ?? ?? ??");
this.hookAgentHudOpenSystemMenu = new Hook<AgentHudOpenSystemMenuPrototype>(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour);

View file

@ -12,19 +12,18 @@ namespace Dalamud.Game.Libc
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class LibcFunction
{
private readonly LibcFunctionAddressResolver address;
private readonly StdStringFromCStringDelegate stdStringCtorCString;
private readonly StdStringDeallocateDelegate stdStringDeallocate;
/// <summary>
/// Initializes a new instance of the <see cref="LibcFunction"/> class.
/// </summary>
public LibcFunction()
[ServiceManager.ServiceConstructor]
private LibcFunction(SigScanner sigScanner)
{
this.address = new LibcFunctionAddressResolver();
this.address.Setup();
this.address.Setup(sigScanner);
this.stdStringCtorCString = Marshal.GetDelegateForFunctionPointer<StdStringFromCStringDelegate>(this.address.StdStringFromCstring);
this.stdStringDeallocate = Marshal.GetDelegateForFunctionPointer<StdStringDeallocateDelegate>(this.address.StdStringDeallocate);

View file

@ -14,6 +14,7 @@ namespace Dalamud.Game.Network
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class GameNetwork : IDisposable
{
private readonly GameNetworkAddressResolver address;
@ -23,13 +24,11 @@ namespace Dalamud.Game.Network
private IntPtr baseAddress;
/// <summary>
/// Initializes a new instance of the <see cref="GameNetwork"/> class.
/// </summary>
internal GameNetwork()
[ServiceManager.ServiceConstructor]
private GameNetwork(SigScanner sigScanner)
{
this.address = new GameNetworkAddressResolver();
this.address.Setup();
this.address.Setup(sigScanner);
Log.Verbose("===== G A M E N E T W O R K =====");
Log.Verbose($"ProcessZonePacketDown address 0x{this.address.ProcessZonePacketDown.ToInt64():X}");

View file

@ -21,6 +21,7 @@ namespace Dalamud.Game.Network.Internal
/// <summary>
/// This class handles network notifications and uploading market board data.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class NetworkHandlers
{
private readonly List<MarketBoardItemRequest> marketBoardRequests = new();
@ -29,14 +30,12 @@ namespace Dalamud.Game.Network.Internal
private MarketBoardPurchaseHandler marketBoardPurchaseHandler;
/// <summary>
/// Initializes a new instance of the <see cref="NetworkHandlers"/> class.
/// </summary>
public NetworkHandlers()
[ServiceManager.ServiceConstructor]
private NetworkHandlers(GameNetwork gameNetwork)
{
this.uploader = new UniversalisMarketBoardUploader();
Service<GameNetwork>.Get().NetworkMessage += this.OnNetworkMessage;
gameNetwork.NetworkMessage += this.OnNetworkMessage;
}
/// <summary>

View file

@ -9,14 +9,13 @@ namespace Dalamud.Game.Network.Internal
/// <summary>
/// This class enables TCP optimizations in the game socket for better performance.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed class WinSockHandlers : IDisposable
{
private Hook<SocketDelegate> ws2SocketHook;
/// <summary>
/// Initializes a new instance of the <see cref="WinSockHandlers"/> class.
/// </summary>
public WinSockHandlers()
[ServiceManager.ServiceConstructor]
private WinSockHandlers()
{
this.ws2SocketHook = Hook<SocketDelegate>.FromSymbol("ws2_32.dll", "socket", this.OnSocket, true);
this.ws2SocketHook?.Enable();

View file

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Utility.Timing;
using Newtonsoft.Json;
using Serilog;
@ -18,6 +19,7 @@ namespace Dalamud.Game
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public class SigScanner : IDisposable
{
private readonly FileInfo? cacheFile;
@ -27,6 +29,38 @@ namespace Dalamud.Game
private Dictionary<string, long>? textCache;
[ServiceManager.ServiceConstructor]
private SigScanner(DalamudStartInfo startInfo)
{
// Initialize the process information.
var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs"));
if (!cacheDir.Exists)
cacheDir.Create();
this.cacheFile = new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json"));
this.Module = Process.GetCurrentProcess().MainModule!;
this.Is32BitProcess = !Environment.Is64BitProcess;
this.IsCopy = true;
// Limit the search space to .text section.
this.SetupSearchSpace(this.Module);
if (this.IsCopy)
this.SetupCopiedSegments();
Log.Verbose($"Module base: 0x{this.TextSectionBase.ToInt64():X}");
Log.Verbose($"Module size: 0x{this.TextSectionSize:X}");
if (this.cacheFile != null)
this.Load();
// Initialize FFXIVClientStructs function resolver
using (Timings.Start("CS Resolver Init"))
{
FFXIVClientStructs.Resolver.InitializeParallel(new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json")));
}
}
/// <summary>
/// Initializes a new instance of the <see cref="SigScanner"/> class using the main module of the current process.
/// </summary>

View file

@ -12,13 +12,12 @@ namespace Dalamud.Game.Text.SeStringHandling
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
[Obsolete("This class is obsolete. Please use the static methods on SeString instead.")]
public sealed class SeStringManager
{
/// <summary>
/// Initializes a new instance of the <see cref="SeStringManager"/> class.
/// </summary>
internal SeStringManager()
[ServiceManager.ServiceConstructor]
private SeStringManager()
{
}

View file

@ -13,14 +13,13 @@ namespace Dalamud.Hooking.Internal
/// <summary>
/// This class manages the final disposition of hooks, cleaning up any that have not reverted their changes.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class HookManager : IDisposable
{
private static readonly ModuleLog Log = new("HM");
/// <summary>
/// Initializes a new instance of the <see cref="HookManager"/> class.
/// </summary>
public HookManager()
[ServiceManager.ServiceConstructor]
private HookManager()
{
}

View file

@ -4,7 +4,7 @@ using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Interface.Internal;
using Dalamud.Utility.Timing;
@ -17,9 +17,10 @@ namespace Dalamud.Interface.GameFonts
/// <summary>
/// Loads game font for use in ImGui.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class GameFontManager : IDisposable
{
private static readonly string[] FontNames =
private static readonly string?[] FontNames =
{
null,
"AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36",
@ -31,8 +32,6 @@ namespace Dalamud.Interface.GameFonts
private readonly object syncRoot = new();
private readonly InterfaceManager interfaceManager;
private readonly FdtReader?[] fdts;
private readonly List<byte[]> texturePixels;
private readonly Dictionary<GameFontStyle, ImFontPtr> fonts = new();
@ -42,40 +41,28 @@ namespace Dalamud.Interface.GameFonts
private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false;
private bool isBuildingAsFallbackFontMode = false;
/// <summary>
/// Initializes a new instance of the <see cref="GameFontManager"/> class.
/// </summary>
public GameFontManager()
[ServiceManager.ServiceConstructor]
private GameFontManager(DataManager dataManager)
{
var dataManager = Service<DataManager>.Get();
using (Timings.Start("Load FDTs"))
using (Timings.Start("Getting fdt data"))
{
this.fdts = FontNames.Select(fontName =>
{
var fileName = $"common/font/{fontName}.fdt";
using (Timings.Start($"Loading FDT: {fileName}"))
{
var file = fontName == null ? null : dataManager.GetFile(fileName);
return file == null ? null : new FdtReader(file!.Data);
}
}).ToArray();
this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray();
}
using (Timings.Start("Getting texture data"))
{
this.texturePixels = Enumerable.Range(1, 1 + this.fdts.Where(x => x != null).Select(x => x.Glyphs.Select(x => x.TextureFileIndex).Max()).Max()).Select(
x =>
{
var fileName = $"common/font/font{x}.tex";
using (Timings.Start($"Get tex: {fileName}"))
{
return dataManager.GameData.GetFile<TexFile>(fileName)!.ImageData;
var texTasks = Enumerable
.Range(1, 1 + this.fdts
.Where(x => x != null)
.Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max())
.Max())
.Select(x => dataManager.GetFile<TexFile>($"common/font/font{x}.tex")!)
.Select(x => new Task<byte[]>(() => x.ImageData!))
.ToArray();
foreach (var task in texTasks)
task.Start();
this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList();
}
}).ToList();
}
this.interfaceManager = Service<InterfaceManager>.Get();
}
/// <summary>
@ -183,6 +170,7 @@ namespace Dalamud.Interface.GameFonts
/// <returns>Handle to game font that may or may not be ready yet.</returns>
public GameFontHandle NewFontRef(GameFontStyle style)
{
var interfaceManager = Service<InterfaceManager>.Get();
var needRebuild = false;
lock (this.syncRoot)
@ -193,7 +181,7 @@ namespace Dalamud.Interface.GameFonts
needRebuild = !this.fonts.ContainsKey(style);
if (needRebuild)
{
if (Service<InterfaceManager>.Get().IsBuildingFontsBeforeAtlasBuild && this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild)
if (interfaceManager.IsBuildingFontsBeforeAtlasBuild && this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild)
{
Log.Information("[GameFontManager] NewFontRef: Building {0} right now, as it is called while BuildFonts is already in progress yet atlas build has not been called yet.", style.ToString());
this.EnsureFont(style);
@ -201,7 +189,7 @@ namespace Dalamud.Interface.GameFonts
else
{
Log.Information("[GameFontManager] NewFontRef: Calling RebuildFonts because {0} has been requested.", style.ToString());
this.interfaceManager.RebuildFonts();
interfaceManager.RebuildFonts();
}
}
@ -294,10 +282,11 @@ namespace Dalamud.Interface.GameFonts
/// </summary>
public unsafe void AfterBuildFonts()
{
var interfaceManager = Service<InterfaceManager>.Get();
var ioFonts = ImGui.GetIO().Fonts;
ioFonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height);
var pixels32 = (uint*)pixels8;
var fontGamma = this.interfaceManager.FontGamma;
var fontGamma = interfaceManager.FontGamma;
foreach (var (style, font) in this.fonts)
{

View file

@ -18,22 +18,12 @@ namespace Dalamud.Interface.Internal
/// <summary>
/// Class handling Dalamud core commands.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class DalamudCommands
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudCommands"/> class.
/// </summary>
public DalamudCommands()
[ServiceManager.ServiceConstructor]
private DalamudCommands(CommandManager commandManager)
{
}
/// <summary>
/// Register all command handlers with the Dalamud instance.
/// </summary>
public void SetupCommands()
{
var commandManager = Service<CommandManager>.Get();
commandManager.AddHandler("/xldclose", new CommandInfo(this.OnUnloadCommand)
{
HelpMessage = Loc.Localize("DalamudUnloadHelp", "Unloads XIVLauncher in-game addon."),

View file

@ -35,6 +35,7 @@ namespace Dalamud.Interface.Internal
/// <summary>
/// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class DalamudInterface : IDisposable
{
private static readonly ModuleLog Log = new("DUI");
@ -75,14 +76,9 @@ namespace Dalamud.Interface.Internal
private bool isImGuiTestWindowsInMonospace = false;
private bool isImGuiDrawMetricsWindow = false;
/// <summary>
/// Initializes a new instance of the <see cref="DalamudInterface"/> class.
/// </summary>
public DalamudInterface()
[ServiceManager.ServiceConstructor]
private DalamudInterface(Dalamud dalamud, DalamudConfiguration configuration, InterfaceManager interfaceManager)
{
var configuration = Service<DalamudConfiguration>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
this.WindowSystem = new WindowSystem("DalamudCore");
this.changelogWindow = new ChangelogWindow() { IsOpen = false };
@ -123,7 +119,6 @@ namespace Dalamud.Interface.Internal
this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup;
interfaceManager.Draw += this.OnDraw;
var dalamud = Service<Dalamud>.Get();
var logoTex =
interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "logo.png"));

View file

@ -7,7 +7,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.ClientState.GamePad;
@ -44,6 +44,7 @@ namespace Dalamud.Interface.Internal
/// <summary>
/// This class manages interaction with the ImGui interface.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal class InterfaceManager : IDisposable
{
private const float MinimumFallbackFontSizePt = 9.6f; // Game's minimum AXIS font size
@ -58,33 +59,27 @@ namespace Dalamud.Interface.Internal
private readonly HashSet<SpecialGlyphRequest> glyphRequests = new();
private readonly Dictionary<ImFontPtr, TargetFontModification> loadedFontInfo = new();
private readonly Hook<PresentDelegate> presentHook;
private readonly Hook<ResizeBuffersDelegate> resizeBuffersHook;
private readonly Hook<SetCursorDelegate> setCursorHook;
private readonly ManualResetEvent fontBuildSignal;
private readonly SwapChainVtableResolver address;
private readonly TaskCompletionSource sceneInitializeTaskCompletionSource = new();
private RawDX11Scene? scene;
private Hook<PresentDelegate>? presentHook;
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
private Hook<SetCursorDelegate>? setCursorHook;
// can't access imgui IO before first present call
private bool lastWantCapture = false;
private bool isRebuildingFonts = false;
private bool isFallbackFontMode = false;
/// <summary>
/// Initializes a new instance of the <see cref="InterfaceManager"/> class.
/// </summary>
public InterfaceManager()
[ServiceManager.ServiceConstructor]
private InterfaceManager()
{
Service<NotificationManager>.Set();
var scanner = Service<SigScanner>.Get();
this.fontBuildSignal = new ManualResetEvent(false);
this.address = new SwapChainVtableResolver();
this.address.Setup(scanner);
try
{
@ -106,16 +101,12 @@ namespace Dalamud.Interface.Internal
Log.Error(e, "RTSS Free failed");
}
this.setCursorHook = Hook<SetCursorDelegate>.FromSymbol("user32.dll", "SetCursor", this.SetCursorDetour, true);
this.presentHook = new Hook<PresentDelegate>(this.address.Present, this.PresentDetour);
this.resizeBuffersHook = new Hook<ResizeBuffersDelegate>(this.address.ResizeBuffers, this.ResizeBuffersDetour);
var setCursorAddress = this.setCursorHook?.Address ?? IntPtr.Zero;
Log.Verbose("===== S W A P C H A I N =====");
Log.Verbose($"SetCursor address 0x{setCursorAddress.ToInt64():X}");
Log.Verbose($"Present address 0x{this.presentHook.Address.ToInt64():X}");
Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook.Address.ToInt64():X}");
Task.Run(async () =>
{
var framework = await Service<Framework>.GetAsync();
var sigScanner = await Service<SigScanner>.GetAsync();
await framework.RunOnFrameworkThread(() => this.Enable(sigScanner));
});
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
@ -129,6 +120,11 @@ namespace Dalamud.Interface.Internal
private delegate void InstallRTSSHook();
/// <summary>
/// Gets a task that gets completed when scene gets initialized.
/// </summary>
public Task SceneInitializeTask => this.sceneInitializeTaskCompletionSource.Task;
/// <summary>
/// This event gets called each frame to facilitate ImGui drawing.
/// </summary>
@ -259,34 +255,6 @@ namespace Dalamud.Interface.Internal
/// </summary>
public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0);
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
{
this.setCursorHook?.Enable();
this.presentHook.Enable();
this.resizeBuffersHook.Enable();
try
{
if (!string.IsNullOrEmpty(this.rtssPath))
{
NativeFunctions.LoadLibraryW(this.rtssPath);
var rtssModule = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll");
var installAddr = NativeFunctions.GetProcAddress(rtssModule, "InstallRTSSHook");
Log.Debug("Installing RTSS hook");
Marshal.GetDelegateForFunctionPointer<InstallRTSSHook>(installAddr).Invoke();
Log.Debug("RTSS hook OK!");
}
}
catch (Exception ex)
{
Log.Error(ex, "Could not reload RTSS");
}
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
@ -303,8 +271,8 @@ namespace Dalamud.Interface.Internal
this.scene?.Dispose();
this.setCursorHook?.Dispose();
this.presentHook.Dispose();
this.resizeBuffersHook.Dispose();
this.presentHook?.Dispose();
this.resizeBuffersHook?.Dispose();
}
#nullable enable
@ -480,9 +448,11 @@ namespace Dalamud.Interface.Internal
try
{
this.scene = new RawDX11Scene(swapChain);
this.sceneInitializeTaskCompletionSource.SetResult();
}
catch (DllNotFoundException ex)
{
this.sceneInitializeTaskCompletionSource.SetException(ex);
Log.Error(ex, "Could not load ImGui dependencies.");
var res = PInvoke.User32.MessageBox(
@ -1003,6 +973,43 @@ namespace Dalamud.Interface.Internal
}
}
private void Enable(SigScanner sigScanner)
{
this.address.Setup(sigScanner);
this.setCursorHook = Hook<SetCursorDelegate>.FromSymbol("user32.dll", "SetCursor", this.SetCursorDetour, true);
this.presentHook = new Hook<PresentDelegate>(this.address.Present, this.PresentDetour);
this.resizeBuffersHook = new Hook<ResizeBuffersDelegate>(this.address.ResizeBuffers, this.ResizeBuffersDetour);
var setCursorAddress = this.setCursorHook?.Address ?? IntPtr.Zero;
Log.Verbose("===== S W A P C H A I N =====");
Log.Verbose($"SetCursor address 0x{setCursorAddress.ToInt64():X}");
Log.Verbose($"Present address 0x{this.presentHook.Address.ToInt64():X}");
Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook.Address.ToInt64():X}");
this.setCursorHook?.Enable();
this.presentHook.Enable();
this.resizeBuffersHook.Enable();
try
{
if (!string.IsNullOrEmpty(this.rtssPath))
{
NativeFunctions.LoadLibraryW(this.rtssPath);
var rtssModule = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll");
var installAddr = NativeFunctions.GetProcAddress(rtssModule, "InstallRTSSHook");
Log.Debug("Installing RTSS hook");
Marshal.GetDelegateForFunctionPointer<InstallRTSSHook>(installAddr).Invoke();
Log.Debug("RTSS hook OK!");
}
}
catch (Exception ex)
{
Log.Error(ex, "Could not reload RTSS");
}
}
private void Disable()
{
this.setCursorHook?.Disable();
@ -1094,6 +1101,9 @@ namespace Dalamud.Interface.Internal
var gamepadState = Service<GamepadState>.GetNullable();
var keyState = Service<KeyState>.GetNullable();
if (dalamudInterface == null || gamepadState == null || keyState == null)
return;
// fix for keys in game getting stuck, if you were holding a game key (like run)
// and then clicked on an imgui textbox - imgui would swallow the keyup event,
// so the game would think the key remained pressed continuously until you left

View file

@ -13,6 +13,7 @@ namespace Dalamud.Interface.Internal.Notifications
/// Class handling notifications/toasts in ImGui.
/// Ported from https://github.com/patrickcjk/imgui-notify.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class NotificationManager
{
/// <summary>
@ -54,6 +55,11 @@ namespace Dalamud.Interface.Internal.Notifications
private readonly List<Notification> notifications = new();
[ServiceManager.ServiceConstructor]
private NotificationManager()
{
}
/// <summary>
/// Add a notification to the notification queue.
/// </summary>

View file

@ -1485,7 +1485,7 @@ namespace Dalamud.Interface.Internal.Windows
ImGuiHelpers.ScaledDummy(20);
// Needed to init the task tracker, if we're not on a debug build
var tracker = Service<TaskTracker>.GetNullable() ?? Service<TaskTracker>.Set();
Service<TaskTracker>.Get().Enable();
for (var i = 0; i < TaskTracker.Tasks.Count; i++)
{

View file

@ -60,6 +60,8 @@ namespace Dalamud.Interface.Internal.Windows
var dalamud = Service<Dalamud>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
interfaceManager.SceneInitializeTask.Wait();
this.DefaultIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))!;
this.TroubleIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))!;
this.UpdateIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))!;

View file

@ -33,6 +33,7 @@ namespace Dalamud.Interface.Internal.Windows
private XivChatType dalamudMessagesChatType;
private bool doWaitForPluginsOnStartup;
private bool doCfTaskBarFlash;
private bool doCfChatMessage;
private bool doMbCollect;
@ -90,6 +91,7 @@ namespace Dalamud.Interface.Internal.Windows
this.dalamudMessagesChatType = configuration.GeneralChatType;
this.doWaitForPluginsOnStartup = configuration.IsResumeGameAfterPluginLoad;
this.doCfTaskBarFlash = configuration.DutyFinderTaskbarFlash;
this.doCfChatMessage = configuration.DutyFinderChatMessage;
this.doMbCollect = configuration.IsMbCollect;
@ -261,6 +263,9 @@ namespace Dalamud.Interface.Internal.Windows
ImGuiHelpers.ScaledDummy(5);
ImGui.Checkbox(Loc.Localize("DalamudSettingsWaitForPluginsOnStartup", "Wait for plugins before game loads"), ref this.doWaitForPluginsOnStartup);
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsWaitForPluginsOnStartupHint", "Do not let the game load, until plugins are loaded."));
ImGui.Checkbox(Loc.Localize("DalamudSettingsFlash", "Flash FFXIV window on duty pop"), ref this.doCfTaskBarFlash);
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFlashHint", "Flash the FFXIV window in your task bar when a duty is ready."));
@ -916,6 +921,7 @@ namespace Dalamud.Interface.Internal.Windows
configuration.GeneralChatType = this.dalamudMessagesChatType;
configuration.IsResumeGameAfterPluginLoad = this.doWaitForPluginsOnStartup;
configuration.DutyFinderTaskbarFlash = this.doCfTaskBarFlash;
configuration.DutyFinderChatMessage = this.doCfChatMessage;
configuration.IsMbCollect = this.doMbCollect;

View file

@ -12,6 +12,7 @@ namespace Dalamud.Interface
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public class TitleScreenMenu
{
/// <summary>
@ -21,6 +22,11 @@ namespace Dalamud.Interface
private readonly List<TitleScreenMenuEntry> entries = new();
[ServiceManager.ServiceConstructor]
private TitleScreenMenu()
{
}
/// <summary>
/// Gets the list of entries in the title screen menu.
/// </summary>

View file

@ -1,5 +1,6 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace Dalamud.IoC.Internal
{
@ -11,11 +12,12 @@ namespace Dalamud.IoC.Internal
/// <summary>
/// Initializes a new instance of the <see cref="ObjectInstance"/> class.
/// </summary>
/// <param name="instance">The underlying instance.</param>
public ObjectInstance(object instance)
/// <param name="instanceTask">Weak reference to the underlying instance.</param>
/// <param name="type">Type of the underlying instance.</param>
public ObjectInstance(Task<WeakReference> instanceTask, Type type)
{
this.Instance = new WeakReference(instance);
this.Version = instance.GetType().GetCustomAttribute<InterfaceVersionAttribute>();
this.InstanceTask = instanceTask;
this.Version = type.GetCustomAttribute<InterfaceVersionAttribute>();
}
/// <summary>
@ -26,6 +28,7 @@ namespace Dalamud.IoC.Internal
/// <summary>
/// Gets a reference to the underlying instance.
/// </summary>
public WeakReference Instance { get; }
/// <returns>The underlying instance.</returns>
public Task<WeakReference> InstanceTask { get; }
}
}

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
namespace Dalamud.IoC.Internal
@ -17,19 +17,26 @@ namespace Dalamud.IoC.Internal
private readonly Dictionary<Type, ObjectInstance> instances = new();
/// <summary>
/// Initializes a new instance of the <see cref="ServiceContainer"/> class.
/// </summary>
public ServiceContainer()
{
}
/// <summary>
/// Register a singleton object of any type into the current IOC container.
/// </summary>
/// <param name="instance">The existing instance to register in the container.</param>
/// <typeparam name="T">The interface to register.</typeparam>
public void RegisterSingleton<T>(T instance)
public void RegisterSingleton<T>(Task<T> instance)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
this.instances[typeof(T)] = new(instance);
this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T));
}
/// <summary>
@ -38,12 +45,12 @@ namespace Dalamud.IoC.Internal
/// <param name="objectType">The type of object to create.</param>
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
/// <returns>The created object.</returns>
public object? Create(Type objectType, params object[] scopedObjects)
public async Task<object?> Create(Type objectType, params object[] scopedObjects)
{
var ctor = this.FindApplicableCtor(objectType, scopedObjects);
if (ctor == null)
{
Log.Error("Failed to create {TypeName}, an eligible ctor with satisfiable services could not be found", objectType.FullName);
Log.Error("Failed to create {TypeName}, an eligible ctor with satisfiable services could not be found", objectType.FullName!);
return null;
}
@ -59,36 +66,37 @@ namespace Dalamud.IoC.Internal
if (!versionCheck)
{
Log.Error("Failed to create {TypeName}, a RequestedVersion could not be satisfied", objectType.FullName);
Log.Error("Failed to create {TypeName}, a RequestedVersion could not be satisfied", objectType.FullName!);
return null;
}
var resolvedParams = parameters
.Select(p =>
var resolvedParams =
await Task.WhenAll(
parameters
.Select(async p =>
{
var service = this.GetService(p.parameterType, scopedObjects);
var service = await this.GetService(p.parameterType, scopedObjects);
if (service == null)
{
Log.Error("Requested service type {TypeName} was not available (null)", p.parameterType.FullName);
Log.Error("Requested service type {TypeName} was not available (null)", p.parameterType.FullName!);
}
return service;
})
.ToArray();
}));
var hasNull = resolvedParams.Any(p => p == null);
if (hasNull)
{
Log.Error("Failed to create {TypeName}, a requested service type could not be satisfied", objectType.FullName);
Log.Error("Failed to create {TypeName}, a requested service type could not be satisfied", objectType.FullName!);
return null;
}
var instance = FormatterServices.GetUninitializedObject(objectType);
if (!this.InjectProperties(instance, scopedObjects))
if (!await this.InjectProperties(instance, scopedObjects))
{
Log.Error("Failed to create {TypeName}, a requested property service type could not be satisfied", objectType.FullName);
Log.Error("Failed to create {TypeName}, a requested property service type could not be satisfied", objectType.FullName!);
return null;
}
@ -105,7 +113,7 @@ namespace Dalamud.IoC.Internal
/// <param name="instance">The object instance.</param>
/// <param name="scopedObjects">Scoped objects.</param>
/// <returns>Whether or not the injection was successful.</returns>
public bool InjectProperties(object instance, params object[] scopedObjects)
public async Task<bool> InjectProperties(object instance, params object[] scopedObjects)
{
var objectType = instance.GetType();
@ -121,17 +129,17 @@ namespace Dalamud.IoC.Internal
if (!versionCheck)
{
Log.Error("Failed to create {TypeName}, a RequestedVersion could not be satisfied", objectType.FullName);
Log.Error("Failed to create {TypeName}, a RequestedVersion could not be satisfied", objectType.FullName!);
return false;
}
foreach (var prop in props)
{
var service = this.GetService(prop.propertyInfo.PropertyType, scopedObjects);
var service = await this.GetService(prop.propertyInfo.PropertyType, scopedObjects);
if (service == null)
{
Log.Error("Requested service type {TypeName} was not available (null)", prop.propertyInfo.PropertyType.FullName);
Log.Error("Requested service type {TypeName} was not available (null)", prop.propertyInfo.PropertyType.FullName!);
return false;
}
@ -162,14 +170,14 @@ namespace Dalamud.IoC.Internal
"Requested version {ReqVersion} does not match the implemented version {ImplVersion} for param type {ParamType}",
requiredVersion.Version,
declVersion.Version,
parameterType.FullName);
parameterType.FullName!);
return false;
}
private object? GetService(Type serviceType, object[] scopedObjects)
private async Task<object?> GetService(Type serviceType, object[] scopedObjects)
{
var singletonService = this.GetService(serviceType);
var singletonService = await this.GetService(serviceType);
if (singletonService != null)
{
return singletonService;
@ -185,15 +193,13 @@ namespace Dalamud.IoC.Internal
return scoped;
}
private object? GetService(Type serviceType)
private async Task<object?> GetService(Type serviceType)
{
var hasInstance = this.instances.TryGetValue(serviceType, out var service);
if (hasInstance && service.Instance.IsAlive)
{
return service.Instance.Target;
}
if (!this.instances.TryGetValue(serviceType, out var service))
return null;
var instance = await service.InstanceTask;
return instance.Target;
}
private ConstructorInfo? FindApplicableCtor(Type type, object[] scopedObjects)
@ -224,7 +230,7 @@ namespace Dalamud.IoC.Internal
var contains = types.Contains(parameter.ParameterType);
if (!contains)
{
Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName);
Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!);
return false;
}
}

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Reflection;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Serilog;
namespace Dalamud
@ -12,6 +13,7 @@ namespace Dalamud
/// <summary>
/// Class handling localization.
/// </summary>
[ServiceManager.EarlyLoadedService]
public class Localization
{
/// <summary>
@ -40,6 +42,16 @@ namespace Dalamud
this.assembly = Assembly.GetCallingAssembly();
}
[ServiceManager.ServiceConstructor]
private Localization(Dalamud dalamud, DalamudConfiguration configuration)
: this(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_")
{
if (!string.IsNullOrEmpty(configuration.LanguageOverride))
this.SetupWithLangCode(configuration.LanguageOverride);
else
this.SetupWithUiCulture();
}
/// <summary>
/// Delegate for the <see cref="Localization.LocalizationChanged"/> event that occurs when the language is changed.
/// </summary>

View file

@ -5,7 +5,7 @@ namespace Dalamud.Logging.Internal
/// <summary>
/// Class offering various methods to allow for logging in Dalamud modules.
/// </summary>
internal class ModuleLog
public class ModuleLog
{
private readonly string moduleName;

View file

@ -12,6 +12,7 @@ namespace Dalamud.Logging.Internal
/// <summary>
/// Class responsible for tracking asynchronous tasks.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class TaskTracker : IDisposable
{
private static readonly ModuleLog Log = new("TT");
@ -20,16 +21,14 @@ namespace Dalamud.Logging.Internal
private static bool clearRequested = false;
private MonoMod.RuntimeDetour.Hook? scheduleAndStartHook;
private bool enabled = false;
/// <summary>
/// Initializes a new instance of the <see cref="TaskTracker"/> class.
/// </summary>
public TaskTracker()
[ServiceManager.ServiceConstructor]
private TaskTracker()
{
this.ApplyPatch();
var framework = Service<Framework>.Get();
framework.Update += this.FrameworkOnUpdate;
#if DEBUG
this.Enable();
#endif
}
/// <summary>
@ -102,6 +101,21 @@ namespace Dalamud.Logging.Internal
}
}
/// <summary>
/// Enables TaskTracker.
/// </summary>
public void Enable()
{
if (this.enabled)
return;
this.ApplyPatch();
var framework = Service<Framework>.Get();
framework.Update += this.FrameworkOnUpdate;
this.enabled = true;
}
/// <inheritdoc/>
public void Dispose()
{

View file

@ -378,7 +378,7 @@ namespace Dalamud.Plugin
realScopedObjects[0] = this;
Array.Copy(scopedObjects, 0, realScopedObjects, 1, scopedObjects.Length);
return svcContainer.InjectProperties(instance, realScopedObjects);
return svcContainer.InjectProperties(instance, realScopedObjects).GetAwaiter().GetResult();
}
#endregion

View file

@ -27,6 +27,7 @@ namespace Dalamud.Plugin.Internal;
/// <summary>
/// Class responsible for loading and unloading plugins.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal partial class PluginManager : IDisposable
{
/// <summary>
@ -40,16 +41,17 @@ internal partial class PluginManager : IDisposable
private readonly DirectoryInfo devPluginDirectory;
private readonly BannedPlugin[] bannedPlugins;
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
public PluginManager()
{
var startInfo = Service<DalamudStartInfo>.Get();
var configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
this.pluginDirectory = new DirectoryInfo(startInfo.PluginDirectory);
this.devPluginDirectory = new DirectoryInfo(startInfo.DefaultPluginDirectory);
[ServiceManager.ServiceDependency]
private readonly DalamudStartInfo startInfo = Service<DalamudStartInfo>.Get();
[ServiceManager.ServiceConstructor]
private PluginManager()
{
this.pluginDirectory = new DirectoryInfo(this.startInfo.PluginDirectory!);
this.devPluginDirectory = new DirectoryInfo(this.startInfo.DefaultPluginDirectory!);
if (!this.pluginDirectory.Exists)
this.pluginDirectory.Create();
@ -57,16 +59,16 @@ internal partial class PluginManager : IDisposable
if (!this.devPluginDirectory.Exists)
this.devPluginDirectory.Create();
this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || configuration.PluginSafeMode;
this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || this.configuration.PluginSafeMode;
if (this.SafeMode)
{
configuration.PluginSafeMode = false;
configuration.Save();
this.configuration.PluginSafeMode = false;
this.configuration.Save();
}
this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath) ?? string.Empty, "pluginConfigs"));
this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(this.startInfo.ConfigurationPath) ?? string.Empty, "pluginConfigs"));
var bannedPluginsJson = File.ReadAllText(Path.Combine(startInfo.AssetDirectory, "UIRes", "bannedplugin.json"));
var bannedPluginsJson = File.ReadAllText(Path.Combine(this.startInfo.AssetDirectory!, "UIRes", "bannedplugin.json"));
this.bannedPlugins = JsonConvert.DeserializeObject<BannedPlugin[]>(bannedPluginsJson) ?? Array.Empty<BannedPlugin>();
this.ApplyPatches();
@ -225,10 +227,8 @@ internal partial class PluginManager : IDisposable
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task SetPluginReposFromConfigAsync(bool notify)
{
var configuration = Service<DalamudConfiguration>.Get();
var repos = new List<PluginRepository>() { PluginRepository.MainRepo };
repos.AddRange(configuration.ThirdRepoList
repos.AddRange(this.configuration.ThirdRepoList
.Where(repo => repo.IsEnabled)
.Select(repo => new PluginRepository(repo.Url, repo.IsEnabled)));
@ -251,8 +251,6 @@ internal partial class PluginManager : IDisposable
return;
}
var configuration = Service<DalamudConfiguration>.Get();
var pluginDefs = new List<PluginDef>();
var devPluginDefs = new List<PluginDef>();
@ -282,7 +280,7 @@ internal partial class PluginManager : IDisposable
// devPlugins are more freeform. Look for any dll and hope to get lucky.
var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
foreach (var setting in configuration.DevPluginLoadLocations)
foreach (var setting in this.configuration.DevPluginLoadLocations)
{
if (!setting.IsEnabled)
continue;
@ -420,15 +418,13 @@ internal partial class PluginManager : IDisposable
return;
}
var configuration = Service<DalamudConfiguration>.Get();
if (!this.devPluginDirectory.Exists)
this.devPluginDirectory.Create();
// devPlugins are more freeform. Look for any dll and hope to get lucky.
var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList();
foreach (var setting in configuration.DevPluginLoadLocations)
foreach (var setting in this.configuration.DevPluginLoadLocations)
{
if (!setting.IsEnabled)
continue;
@ -682,9 +678,6 @@ internal partial class PluginManager : IDisposable
/// </summary>
public void CleanupPlugins()
{
var configuration = Service<DalamudConfiguration>.Get();
var startInfo = Service<DalamudStartInfo>.Get();
foreach (var pluginDir in this.pluginDirectory.GetDirectories())
{
try
@ -748,7 +741,7 @@ internal partial class PluginManager : IDisposable
continue;
}
if (manifest.ApplicableVersion < startInfo.GameVersion)
if (manifest.ApplicableVersion <this.startInfo.GameVersion)
{
Log.Information($"Inapplicable version: cleaning up {versionDir.FullName}");
versionDir.Delete(true);
@ -919,15 +912,12 @@ internal partial class PluginManager : IDisposable
/// <returns>If the manifest is eligible.</returns>
public bool IsManifestEligible(PluginManifest manifest)
{
var configuration = Service<DalamudConfiguration>.Get();
var startInfo = Service<DalamudStartInfo>.Get();
// Testing exclusive
if (manifest.IsTestingExclusive && !configuration.DoPluginTest)
return false;
// Applicable version
if (manifest.ApplicableVersion < startInfo.GameVersion)
if (manifest.ApplicableVersion <this.startInfo.GameVersion)
return false;
// API level
@ -945,7 +935,6 @@ internal partial class PluginManager : IDisposable
/// <returns>A value indicating whether the plugin/manifest has been banned.</returns>
public bool IsManifestBanned(PluginManifest manifest)
{
var configuration = Service<DalamudConfiguration>.Get();
return !configuration.LoadBannedPlugins && this.bannedPlugins.Any(ban => (ban.Name == manifest.InternalName || ban.Name == Hash.GetStringSha256Hash(manifest.InternalName))
&& ban.AssemblyVersion >= manifest.AssemblyVersion);
}

View file

@ -0,0 +1,49 @@
using System;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Support;
using Dalamud.Utility.Timing;
namespace Dalamud.Plugin.Internal;
/// <summary>
/// Class responsible for loading plugins on startup.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
public class StartupPluginLoader
{
private static readonly ModuleLog Log = new("SPL");
[ServiceManager.ServiceConstructor]
private StartupPluginLoader(PluginManager pluginManager)
{
try
{
using (Timings.Start("PM Load Plugin Repos"))
{
_ = pluginManager.SetPluginReposFromConfigAsync(false);
pluginManager.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting);
Log.Information("[T3] PM repos OK!");
}
using (Timings.Start("PM Cleanup Plugins"))
{
pluginManager.CleanupPlugins();
Log.Information("[T3] PMC OK!");
}
using (Timings.Start("PM Load Sync Plugins"))
{
pluginManager.LoadAllPlugins();
Log.Information("[T3] PML OK!");
}
Task.Run(Troubleshooting.LogTroubleshooting);
}
catch (Exception ex)
{
Log.Error(ex, "Plugin load failed");
}
}
}

View file

@ -334,7 +334,7 @@ internal class LocalPlugin : IDisposable
this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev);
var ioc = Service<ServiceContainer>.Get();
this.instance = ioc.Create(this.pluginType, this.DalamudInterface) as IDalamudPlugin;
this.instance = ioc.Create(this.pluginType, this.DalamudInterface).GetAwaiter().GetResult() as IDalamudPlugin;
if (this.instance == null)
{
this.State = PluginState.LoadError;

View file

@ -5,14 +5,13 @@ namespace Dalamud.Plugin.Ipc.Internal
/// <summary>
/// This class facilitates inter-plugin communication.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class CallGate
{
private readonly Dictionary<string, CallGateChannel> gates = new();
/// <summary>
/// Initializes a new instance of the <see cref="CallGate"/> class.
/// </summary>
internal CallGate()
[ServiceManager.ServiceConstructor]
private CallGate()
{
}

177
Dalamud/ServiceManager.cs Normal file
View file

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using JetBrains.Annotations;
namespace Dalamud
{
/// <summary>
/// Class to initialize Service&lt;T&gt;s.
/// </summary>
internal static class ServiceManager
{
/// <summary>
/// Static log facility for Service{T}, to avoid duplicate instances for different types.
/// </summary>
public static readonly ModuleLog Log = new("SVC");
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
/// <summary>
/// Gets task that gets completed when all blocking early loading services are done loading.
/// </summary>
public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task;
/// <summary>
/// Kicks off construction of services that can handle early loading.
/// </summary>
/// <returns>Task for initializing all services.</returns>
public static async Task InitializeEarlyLoadableServices()
{
Service<ServiceContainer>.Provide(new ServiceContainer());
var service = typeof(Service<>);
var blockingEarlyLoadingServices = new List<Task>();
var dependencyServicesMap = new Dictionary<Type, List<Type>>();
var getAsyncTaskMap = new Dictionary<Type, Task>();
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
{
var attr = serviceType.GetCustomAttribute<Service>(true)?.GetType();
if (attr?.IsAssignableTo(typeof(EarlyLoadedService)) != true)
continue;
var getTask = (Task)service.MakeGenericType(serviceType).InvokeMember(
"GetAsync",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null);
if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService)))
{
getAsyncTaskMap[serviceType] = getTask;
blockingEarlyLoadingServices.Add(getTask);
}
dependencyServicesMap[serviceType] =
(List<Type>)service
.MakeGenericType(serviceType)
.InvokeMember(
"GetDependencyServices",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null);
}
_ = Task.WhenAll(blockingEarlyLoadingServices).ContinueWith(x =>
{
try
{
if (x.IsFaulted)
BlockingServicesLoadedTaskCompletionSource.SetException(x.Exception!);
else
BlockingServicesLoadedTaskCompletionSource.SetResult();
}
catch (Exception)
{
// don't care, as this means task result/exception has already been set
}
}).ConfigureAwait(false);
try
{
var tasks = new List<Task>();
while (dependencyServicesMap.Any())
{
tasks.Clear();
foreach (var (serviceType, dependencies) in dependencyServicesMap.ToList())
{
if (!dependencies.All(
x => !getAsyncTaskMap.ContainsKey(x) || getAsyncTaskMap[x].IsCompleted))
continue;
tasks.Add((Task)service.MakeGenericType(serviceType).InvokeMember(
"StartLoader",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null));
dependencyServicesMap.Remove(serviceType);
}
if (!tasks.Any())
throw new InvalidOperationException("Unresolvable dependency cycle detected");
await Task.WhenAll(tasks);
foreach (var task in tasks)
{
if (task.IsFaulted)
throw task.Exception!;
}
}
}
catch (Exception e)
{
Log.Error(e, "Failed resolving services");
try
{
BlockingServicesLoadedTaskCompletionSource.SetException(e);
}
catch (Exception)
{
// don't care, as this means task result/exception has already been set
}
throw;
}
}
/// <summary>
/// Indicates that this constructor will be called for early initialization.
/// </summary>
[AttributeUsage(AttributeTargets.Constructor)]
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public class ServiceConstructor : Attribute
{
}
/// <summary>
/// Indicates that the field is a service that should be loaded before constructing the class.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class ServiceDependency : Attribute
{
}
/// <summary>
/// Indicates that the class is a service.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class Service : Attribute
{
}
/// <summary>
/// Indicates that the class is a service, and will be instantiated automatically on startup.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class EarlyLoadedService : Service
{
}
/// <summary>
/// Indicates that the class is a service, and will be instantiated automatically on startup,
/// blocking game main thread until it completes.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class BlockingEarlyLoadedService : EarlyLoadedService
{
}
}
}

View file

@ -1,9 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Utility.Timing;
using JetBrains.Annotations;
namespace Dalamud
{
@ -14,112 +18,154 @@ namespace Dalamud
/// Only used internally within Dalamud, if plugins need access to things it should be _only_ via DI.
/// </remarks>
/// <typeparam name="T">The class you want to store in the service locator.</typeparam>
internal static class Service<T> where T : class
internal static class Service<T>
{
private static readonly ModuleLog Log = new("SVC");
// ReSharper disable once StaticMemberInGenericType
private static readonly TaskCompletionSource<T> InstanceTcs = new();
private static T? instance;
// ReSharper disable once StaticMemberInGenericType
private static bool startLoaderInvoked = false;
static Service()
{
var exposeToPlugins = typeof(T).GetCustomAttribute<PluginInterfaceAttribute>() != null;
if (exposeToPlugins)
ServiceManager.Log.Debug("Service<{0}>: Static ctor called; will be exposed to plugins", typeof(T).Name);
else
ServiceManager.Log.Debug("Service<{0}>: Static ctor called", typeof(T).Name);
if (exposeToPlugins)
Service<ServiceContainer>.Get().RegisterSingleton(InstanceTcs.Task);
}
/// <summary>
/// Initializes the service.
/// </summary>
/// <returns>The object.</returns>
[UsedImplicitly]
public static Task<T> StartLoader()
{
if (startLoaderInvoked)
throw new InvalidOperationException("StartLoader has already been called.");
var attr = typeof(T).GetCustomAttribute<ServiceManager.Service>(true)?.GetType();
if (attr?.IsAssignableTo(typeof(ServiceManager.EarlyLoadedService)) != true)
throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService");
startLoaderInvoked = true;
return Task.Run(async () =>
{
using (Timings.Start($"{typeof(T).Namespace} Enable"))
{
if (attr?.IsAssignableTo(typeof(ServiceManager.BlockingEarlyLoadedService)) == true)
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name);
try
{
var x = await ConstructObject();
if (attr?.IsAssignableTo(typeof(ServiceManager.BlockingEarlyLoadedService)) == true)
ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name);
InstanceTcs.SetResult(x);
return x;
}
catch (Exception e)
{
InstanceTcs.SetException(e);
if (attr?.IsAssignableTo(typeof(ServiceManager.BlockingEarlyLoadedService)) == true)
ServiceManager.Log.Error(e, "Service<{0}>: Construction failure", typeof(T).Name);
throw;
}
}
});
}
/// <summary>
/// Sets the type in the service locator to the given object.
/// </summary>
/// <param name="obj">Object to set.</param>
/// <returns>The set object.</returns>
public static T Set(T obj)
public static void Provide(T obj)
{
SetInstanceObject(obj);
return instance!;
InstanceTcs!.SetResult(obj);
ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name);
}
/// <summary>
/// Sets the type in the service locator via the default parameterless constructor.
/// Pull the instance out of the service locator, waiting if necessary.
/// </summary>
/// <returns>The set object.</returns>
public static T Set()
{
if (instance != null)
throw new Exception($"Service {typeof(T).FullName} was set twice");
var obj = (T?)Activator.CreateInstance(typeof(T), true);
SetInstanceObject(obj);
return instance!;
}
/// <summary>
/// Sets a type in the service locator via a constructor with the given parameter types.
/// </summary>
/// <param name="args">Constructor arguments.</param>
/// <returns>The set object.</returns>
public static T Set(params object[] args)
{
if (args == null)
{
throw new ArgumentNullException(nameof(args), $"Service locator was passed a null for type {typeof(T).FullName} parameterized constructor ");
}
var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding;
var obj = (T?)Activator.CreateInstance(typeof(T), flags, null, args, null, null);
SetInstanceObject(obj);
return obj;
}
/// <summary>
/// Attempt to pull the instance out of the service locator.
/// </summary>
/// <returns>The object if registered.</returns>
/// <exception cref="InvalidOperationException">Thrown when the object instance is not present in the service locator.</exception>
/// <returns>The object.</returns>
public static T Get()
{
return instance ?? throw new InvalidOperationException($"{typeof(T).FullName} has not been registered in the service locator!");
if (!InstanceTcs.Task.IsCompleted)
InstanceTcs.Task.Wait();
return InstanceTcs.Task.Result;
}
/// <summary>
/// Pull the instance out of the service locator, waiting if necessary.
/// </summary>
/// <returns>The object.</returns>
[UsedImplicitly]
public static Task<T> GetAsync() => InstanceTcs.Task;
/// <summary>
/// Attempt to pull the instance out of the service locator.
/// </summary>
/// <returns>The object if registered, null otherwise.</returns>
public static T? GetNullable()
public static T? GetNullable() => InstanceTcs.Task.IsCompleted ? InstanceTcs.Task.Result : default;
/// <summary>
/// Gets an enumerable containing Service&lt;T&gt;s that are required for this Service to initialize without blocking.
/// </summary>
/// <returns>List of dependency services.</returns>
public static List<Type> GetDependencyServices()
{
return instance;
var res = new List<Type>();
res.AddRange(GetServiceConstructor()
.GetParameters()
.Select(x => x.ParameterType));
res.AddRange(typeof(T)
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Select(x => x.FieldType)
.Where(x => x.GetCustomAttribute<ServiceManager.ServiceDependency>(true) != null));
return res
.Distinct()
.Select(x => typeof(Service<>).MakeGenericType(x))
.ToList();
}
private static void SetInstanceObject(T instance)
private static async Task<object?> GetServiceObjectConstructArgument(Type type)
{
Service<T>.instance = instance ?? throw new ArgumentNullException(nameof(instance), $"Service locator received a null for type {typeof(T).FullName}");
var availableToPlugins = RegisterInIoCContainer(instance);
if (availableToPlugins)
Log.Information($"Registered {typeof(T).FullName} into service locator and exposed to plugins");
else
Log.Information($"Registered {typeof(T).FullName} into service locator privately");
var task = (Task)typeof(Service<>)
.MakeGenericType(type)
.InvokeMember(
"GetAsync",
BindingFlags.InvokeMethod |
BindingFlags.Static |
BindingFlags.Public,
null,
null,
null)!;
await task;
return typeof(Task<>).MakeGenericType(type)
.GetProperty("Result", BindingFlags.Instance | BindingFlags.Public)!
.GetValue(task);
}
private static bool RegisterInIoCContainer(T instance)
private static ConstructorInfo GetServiceConstructor()
{
var attr = typeof(T).GetCustomAttribute<PluginInterfaceAttribute>();
if (attr == null)
{
return false;
const BindingFlags ctorBindingFlags =
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding;
return typeof(T)
.GetConstructors(ctorBindingFlags)
.Single(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any());
}
var ioc = Service<ServiceContainer>.GetNullable();
if (ioc == null)
private static async Task<T> ConstructObject()
{
return false;
}
ioc.RegisterSingleton(instance);
return true;
var ctor = GetServiceConstructor();
var args = await Task.WhenAll(
ctor.GetParameters().Select(x => GetServiceObjectConstructArgument(x.ParameterType)));
return (T)ctor.Invoke(args)!;
}
}
}

View file

@ -8,12 +8,12 @@ namespace Dalamud.Utility;
public static class ThreadSafety
{
[ThreadStatic]
private static bool isMainThread;
private static bool threadStaticIsMainThread;
/// <summary>
/// Gets a value indicating whether the current thread is the main thread.
/// </summary>
public static bool IsMainThread => isMainThread;
public static bool IsMainThread => threadStaticIsMainThread;
/// <summary>
/// Throws an exception when the current thread is not the main thread.
@ -21,7 +21,7 @@ public static class ThreadSafety
/// <exception cref="InvalidOperationException">Thrown when the current thread is not the main thread.</exception>
public static void AssertMainThread()
{
if (!isMainThread)
if (!threadStaticIsMainThread)
{
throw new InvalidOperationException("Not on main thread!");
}
@ -33,7 +33,7 @@ public static class ThreadSafety
/// <exception cref="InvalidOperationException">Thrown when the current thread is the main thread.</exception>
public static void AssertNotMainThread()
{
if (isMainThread)
if (threadStaticIsMainThread)
{
throw new InvalidOperationException("On main thread!");
}
@ -44,6 +44,6 @@ public static class ThreadSafety
/// </summary>
internal static void MarkMainThread()
{
isMainThread = true;
threadStaticIsMainThread = true;
}
}