Initial commit: AetherBags + KamiToolKit for FC Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public unsafe partial class NativeAddon {
|
||||
private readonly JsonSerializerOptions serializerOptions = new() {
|
||||
WriteIndented = true,
|
||||
IncludeFields = true,
|
||||
};
|
||||
|
||||
private AddonConfig LoadAddonConfig() {
|
||||
var directory = DalamudInterface.Instance.PluginInterface.ConfigDirectory;
|
||||
var file = new FileInfo(Path.Combine(directory.FullName, $"{InternalName}.addon.json"));
|
||||
if (!file.Exists) {
|
||||
file.Create().Close();
|
||||
|
||||
var newConfig = new AddonConfig();
|
||||
SaveAddonConfig(newConfig);
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
AddonConfig? addonConfig;
|
||||
|
||||
try {
|
||||
var data = File.ReadAllText(file.FullName);
|
||||
addonConfig = JsonSerializer.Deserialize<AddonConfig>(data, serializerOptions);
|
||||
addonConfig ??= new AddonConfig();
|
||||
}
|
||||
catch (Exception e) {
|
||||
DalamudInterface.Instance.Log.Error(e, "Exception while deserializing AddonConfig, creating new config.");
|
||||
addonConfig = new AddonConfig();
|
||||
SaveAddonConfig(addonConfig);
|
||||
}
|
||||
|
||||
return addonConfig;
|
||||
}
|
||||
|
||||
private void SaveAddonConfig(AddonConfig addonConfig) {
|
||||
var directory = DalamudInterface.Instance.PluginInterface.ConfigDirectory;
|
||||
var file = new FileInfo(Path.Combine(directory.FullName, $"{InternalName}.addon.json"));
|
||||
|
||||
var data = JsonSerializer.Serialize(addonConfig, serializerOptions);
|
||||
|
||||
FilesystemUtil.WriteAllTextSafe(file.FullName, data);
|
||||
}
|
||||
|
||||
private void SaveAddonConfig() {
|
||||
var configData = new AddonConfig {
|
||||
Position = new Vector2(InternalAddon->X, InternalAddon->Y),
|
||||
Scale = InternalAddon->Scale / AtkUnitBase.GetGlobalUIScale(),
|
||||
};
|
||||
|
||||
SaveAddonConfig(configData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Linq;
|
||||
using Dalamud.Hooking;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
|
||||
private static Hook<AtkUnitBase.Delegates.FireCallback>? fireCallbackHook;
|
||||
|
||||
private static void InitializeCloseCallback() {
|
||||
fireCallbackHook ??= DalamudInterface.Instance.GameInteropProvider
|
||||
.HookFromAddress<AtkUnitBase.Delegates.FireCallback>(AtkUnitBase.Addresses.FireCallback.Value, OnFireCallback);
|
||||
fireCallbackHook.Enable();
|
||||
}
|
||||
|
||||
private static bool OnFireCallback(AtkUnitBase* thisPtr, uint valueCount, AtkValue* values, bool close) {
|
||||
Log.Excessive($"[{thisPtr->NameString}] OnFireCallback");
|
||||
|
||||
foreach (var addon in CreatedAddons) {
|
||||
if (addon == thisPtr && close && addon is { RespectCloseAll: true, IsOverlayAddon: false }) {
|
||||
addon.Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return fireCallbackHook!.Original(thisPtr, valueCount, values, close);
|
||||
}
|
||||
|
||||
private static void DisposeCloseCallback() {
|
||||
if (CreatedAddons.Count is 0 || CreatedAddons.All(addon => addon.IsOverlayAddon)) {
|
||||
fireCallbackHook?.Dispose();
|
||||
fireCallbackHook = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract partial class NativeAddon : IDisposable {
|
||||
|
||||
private static readonly List<NativeAddon> CreatedAddons = [];
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
public virtual void Dispose() {
|
||||
if (IsOverlayAddon) {
|
||||
// Intentionally leak OverlayAddons,
|
||||
// until Dalamud can implement OverlayAddons globally.
|
||||
CreatedAddons.Remove(this);
|
||||
GC.SuppressFinalize(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDisposed) {
|
||||
Log.Debug($"Disposing addon {GetType()}");
|
||||
|
||||
Close();
|
||||
|
||||
// Close will remove this node automatically on AtkUnitBase.Finalize,
|
||||
// However, this is after the plugin unloads,
|
||||
// and will trigger a warning in auto-dispose if we don't remove this now.
|
||||
CreatedAddons.Remove(this);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
isDisposed = true;
|
||||
DisposeCloseCallback();
|
||||
}
|
||||
|
||||
~NativeAddon() => Dispose();
|
||||
|
||||
internal static void DisposeAddons() {
|
||||
foreach (var addon in CreatedAddons.ToArray()) {
|
||||
if (addon.IsOverlayAddon) continue;
|
||||
|
||||
Log.Warning($"Addon {addon.GetType()} was not disposed properly please ensure you call dispose at an appropriate time.");
|
||||
Log.Debug($"Automatically disposing addon {addon.GetType()} as a safety measure.");
|
||||
|
||||
addon.Dispose();
|
||||
}
|
||||
|
||||
CreatedAddons.Clear();
|
||||
DisposeCloseCallback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public unsafe partial class NativeAddon {
|
||||
|
||||
private void UpdateFlags() {
|
||||
|
||||
// Disable Native AddonConfig
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x40, true);
|
||||
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A1, 0x4, DisableClose);
|
||||
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x8, DisableCloseTransition);
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x40, DisableAddonConfig);
|
||||
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A3, 0x20, DisableClamping);
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A3, 0x1, EnableContextMenu);
|
||||
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1C8, 0x800, DisableScaleContextOption);
|
||||
|
||||
if (IsOverlayAddon) {
|
||||
SetOverlayFlags();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetOverlayFlags() {
|
||||
|
||||
OpenWindowSoundEffectId = 0;
|
||||
InternalAddon->ShowSoundEffectId = 0;
|
||||
|
||||
// Disable ability to focus window
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A0, 0x80, true);
|
||||
|
||||
// Don't load into FocusedAddons list
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A1, 0x40, true);
|
||||
|
||||
// Disable Controller Nav
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x2, true);
|
||||
|
||||
// Disable open/close transitions
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x8, true);
|
||||
|
||||
// Disable open/close sounds
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x20, true);
|
||||
|
||||
// Enable ClickThrough
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A3, 0x40, true);
|
||||
}
|
||||
|
||||
public bool DisableClose { get; init; }
|
||||
|
||||
public bool DisableCloseTransition { get; init; }
|
||||
|
||||
internal bool DisableAddonConfig { get; init; } = true;
|
||||
|
||||
public bool EnableContextMenu { get; init; } = true;
|
||||
|
||||
public bool DisableClamping { get; init; } = true;
|
||||
|
||||
public bool DisableScaleContextOption { get; init; }
|
||||
|
||||
public bool RespectCloseAll { get; set; } = true;
|
||||
|
||||
public bool IgnoreGlobalScale { get; set; } = false;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
|
||||
protected virtual void OnSetup(AtkUnitBase* addon) { }
|
||||
protected virtual void OnShow(AtkUnitBase* addon) { }
|
||||
protected virtual void OnDraw(AtkUnitBase* addon) { }
|
||||
protected virtual void OnUpdate(AtkUnitBase* addon) { }
|
||||
protected virtual void OnHide(AtkUnitBase* addon) { }
|
||||
protected virtual void OnFinalize(AtkUnitBase* addon) { }
|
||||
protected virtual void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { }
|
||||
protected virtual void OnRefresh(AtkUnitBase* addon, Span<AtkValue> atkValues) { }
|
||||
|
||||
private bool isSetup;
|
||||
|
||||
private void Initialize(AtkUnitBase* thisPtr) {
|
||||
Log.Verbose($"[{InternalName}] Initialize");
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Initialize(thisPtr);
|
||||
|
||||
thisPtr->UldManager.InitializeResourceRendererManager();
|
||||
|
||||
InitializeAddon();
|
||||
}
|
||||
|
||||
private void Setup(AtkUnitBase* addon, uint valueCount, AtkValue* values) {
|
||||
Log.Verbose($"[{InternalName}] Setup");
|
||||
|
||||
if (!IsOverlayAddon) {
|
||||
SetInitialState();
|
||||
}
|
||||
else {
|
||||
ref var screenSize = ref AtkStage.Instance()->ScreenSize;
|
||||
|
||||
addon->SetScale(1.0f / AtkUnitBase.GetGlobalUIScale(), true);
|
||||
addon->SetSize((ushort)screenSize.Width, (ushort)screenSize.Height);
|
||||
addon->SetPosition(0, 0);
|
||||
}
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->OnSetup(addon, valueCount, values);
|
||||
|
||||
OnSetup(addon);
|
||||
isSetup = true;
|
||||
}
|
||||
|
||||
private void Show(AtkUnitBase* addon, bool silenceOpenSoundEffect, uint unsetShowHideFlags) {
|
||||
Log.Verbose($"[{InternalName}] Show");
|
||||
|
||||
OnShow(addon);
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Show(addon, silenceOpenSoundEffect, unsetShowHideFlags);
|
||||
}
|
||||
|
||||
private void Update(AtkUnitBase* addon, float delta) {
|
||||
Log.Excessive($"[{InternalName}] Update");
|
||||
|
||||
OnUpdate(addon);
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Update(addon, delta);
|
||||
}
|
||||
|
||||
private void Draw(AtkUnitBase* addon) {
|
||||
Log.Excessive($"[{InternalName}] Draw");
|
||||
|
||||
OnDraw(addon);
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Draw(addon);
|
||||
}
|
||||
|
||||
private void Hide(AtkUnitBase* addon, bool unkBool, bool callHideCallback, uint setShowHideFlags) {
|
||||
Log.Verbose($"[{InternalName}] Hide");
|
||||
|
||||
OnHide(addon);
|
||||
SaveAddonConfig();
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Hide(addon, unkBool, callHideCallback, setShowHideFlags);
|
||||
AtkUnitBase.StaticVirtualTablePointer->Close(addon, false);
|
||||
}
|
||||
|
||||
private void Hide2(AtkUnitBase* addon) {
|
||||
Log.Verbose($"[{InternalName}] Hide2");
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Hide2(addon);
|
||||
}
|
||||
|
||||
private void Finalizer(AtkUnitBase* addon) {
|
||||
Log.Verbose($"[{InternalName}] Finalize");
|
||||
|
||||
OnFinalize(addon);
|
||||
|
||||
if (RememberClosePosition) {
|
||||
LastClosePosition = new Vector2(InternalAddon->X, InternalAddon->Y);
|
||||
}
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Finalizer(InternalAddon);
|
||||
isSetup = false;
|
||||
}
|
||||
|
||||
private AtkEventListener* Destructor(AtkUnitBase* addon, byte flags) {
|
||||
Log.Verbose($"[{InternalName}] Destructor");
|
||||
|
||||
var result = AtkUnitBase.StaticVirtualTablePointer->Dtor(addon, flags);
|
||||
|
||||
if ((flags & 1) == 1) {
|
||||
InternalAddon = null;
|
||||
disposeHandle?.Free();
|
||||
disposeHandle = null;
|
||||
CreatedAddons.Remove(this);
|
||||
|
||||
// Free our custom virtual table, the game doesn't know this exists and won't clear it on its own.
|
||||
NativeMemoryHelper.Free(virtualTable, 0x8 * VirtualTableEntryCount);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void RequestedUpdate(AtkUnitBase* thisPtr, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) {
|
||||
Log.Verbose($"[{InternalName}] RequestedUpdate");
|
||||
|
||||
// Prevent calls to OnRequestedUpdate before Setup is completed. The game will try to call this after Show but before Setup
|
||||
if (isSetup) {
|
||||
OnRequestedUpdate(thisPtr, numberArrayData, stringArrayData);
|
||||
}
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->OnRequestedUpdate(InternalAddon, numberArrayData, stringArrayData);
|
||||
}
|
||||
|
||||
private bool Refresh(AtkUnitBase* thisPtr, uint valueCount, AtkValue* values) {
|
||||
Log.Verbose($"[{InternalName}] Refresh");
|
||||
|
||||
OnRefresh(thisPtr, new Span<AtkValue>(values, (int)valueCount));
|
||||
|
||||
return AtkUnitBase.StaticVirtualTablePointer->OnRefresh(InternalAddon, valueCount, values);
|
||||
}
|
||||
|
||||
private void ScreenSizeChange(AtkUnitBase* thisPtr, int width, int height) {
|
||||
Log.Verbose($"[{InternalName}] ScreenSizeChange");
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->OnScreenSizeChange(thisPtr, width, height);
|
||||
|
||||
if (IsOverlayAddon || IgnoreGlobalScale) {
|
||||
thisPtr->SetScale(1.0f / AtkUnitBase.GetGlobalUIScale(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
public void SetWindowPosition(Vector2 windowPosition) {
|
||||
if (InternalAddon is null) return;
|
||||
InternalAddon->SetPosition((short)windowPosition.X, (short)windowPosition.Y);
|
||||
}
|
||||
|
||||
public void SetWindowSize(Vector2 windowSize) {
|
||||
if (InternalAddon is null) return;
|
||||
|
||||
Size = windowSize;
|
||||
InternalAddon->SetSize((ushort)Size.X, (ushort)Size.Y);
|
||||
|
||||
WindowNode?.Size = Size;
|
||||
}
|
||||
|
||||
protected void SetWindowSize(float width, float height)
|
||||
=> SetWindowSize(new Vector2(width, height));
|
||||
|
||||
public required string InternalName {
|
||||
get;
|
||||
init => field = new string(value.Replace(" ", "").Take(31).ToArray());
|
||||
}
|
||||
|
||||
public required ReadOnlySeString Title { get; set; }
|
||||
|
||||
public ReadOnlySeString? Subtitle { get; set; }
|
||||
|
||||
public int OpenWindowSoundEffectId { get; set; } = 23;
|
||||
|
||||
public Vector2 Size { get; set; } = new(400.0f, 400.0f);
|
||||
|
||||
public Vector2 ContentStartPosition => (WindowNode?.ContentStartPosition ?? Vector2.Zero) + ContentPadding;
|
||||
|
||||
public Vector2 ContentSize => (WindowNode?.ContentSize ?? Vector2.Zero) - ContentPadding * 2.0f - new Vector2(0.0f, 4.0f);
|
||||
|
||||
public Vector2 ContentPadding { get; set; } = new(8.0f, 0.0f);
|
||||
|
||||
public int DepthLayer { get; init; } = 5;
|
||||
|
||||
public bool IsOpen => InternalAddon is not null && InternalAddon->IsVisible;
|
||||
|
||||
public int AddonId => InternalAddon is null ? 0 : InternalAddon->Id;
|
||||
|
||||
public bool RememberClosePosition { get; set; } = true;
|
||||
|
||||
internal Vector2 LastClosePosition = Vector2.Zero;
|
||||
|
||||
public static implicit operator AtkUnitBase*(NativeAddon addon) => addon.InternalAddon;
|
||||
|
||||
internal bool IsOverlayAddon { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
|
||||
private const int VirtualTableEntryCount = 200;
|
||||
|
||||
private AtkUnitBase.Delegates.Dtor destructorFunction = null!;
|
||||
private AtkUnitBase.Delegates.Draw drawFunction = null!;
|
||||
private AtkUnitBase.Delegates.Finalizer finalizerFunction = null!;
|
||||
private AtkUnitBase.Delegates.Hide hideFunction = null!;
|
||||
private AtkUnitBase.Delegates.Initialize initializeFunction = null!;
|
||||
private AtkUnitBase.Delegates.OnSetup onSetupFunction = null!;
|
||||
private AtkUnitBase.Delegates.Show showFunction = null!;
|
||||
private AtkUnitBase.Delegates.Hide2 softHideFunction = null!;
|
||||
private AtkUnitBase.Delegates.Update updateFunction = null!;
|
||||
private AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction = null!;
|
||||
private AtkUnitBase.Delegates.OnRefresh onRefreshFunction = null!;
|
||||
private AtkUnitBase.Delegates.OnScreenSizeChange onScreenSizeChangedFunction = null!;
|
||||
|
||||
private AtkUnitBase.AtkUnitBaseVirtualTable* virtualTable;
|
||||
|
||||
private void RegisterVirtualTable() {
|
||||
|
||||
// Overwrite virtual table with a custom copy,
|
||||
// Note: currently there are 73 vfuncs, but there's no harm in copying more for when they add new vfuncs to the game
|
||||
virtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)NativeMemoryHelper.Malloc(0x8 * VirtualTableEntryCount);
|
||||
NativeMemory.Copy(InternalAddon->VirtualTable, virtualTable, 0x8 * VirtualTableEntryCount);
|
||||
InternalAddon->VirtualTable = virtualTable;
|
||||
|
||||
initializeFunction = Initialize;
|
||||
onSetupFunction = Setup;
|
||||
showFunction = Show;
|
||||
updateFunction = Update;
|
||||
drawFunction = Draw;
|
||||
hideFunction = Hide;
|
||||
softHideFunction = Hide2;
|
||||
finalizerFunction = Finalizer;
|
||||
destructorFunction = Destructor;
|
||||
onRequestedUpdateFunction = RequestedUpdate;
|
||||
onRefreshFunction = Refresh;
|
||||
onScreenSizeChangedFunction = ScreenSizeChange;
|
||||
|
||||
virtualTable->Initialize = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(initializeFunction);
|
||||
virtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(onSetupFunction);
|
||||
virtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(showFunction);
|
||||
virtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(updateFunction);
|
||||
virtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(drawFunction);
|
||||
virtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(hideFunction);
|
||||
virtualTable->Hide2 = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(softHideFunction);
|
||||
virtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(finalizerFunction);
|
||||
virtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(destructorFunction);
|
||||
virtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(onRequestedUpdateFunction);
|
||||
virtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(onRefreshFunction);
|
||||
virtualTable->OnScreenSizeChange = (delegate* unmanaged<AtkUnitBase*, int, int, void>)Marshal.GetFunctionPointerForDelegate(onScreenSizeChangedFunction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
using KamiToolKit.Timelines;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
|
||||
private GCHandle? disposeHandle;
|
||||
|
||||
internal AtkUnitBase* InternalAddon;
|
||||
|
||||
public ResNode RootNode = null!;
|
||||
|
||||
protected WindowNodeBase? WindowNode { get; private set; }
|
||||
|
||||
private void AllocateAddon() {
|
||||
if (InternalAddon is not null) {
|
||||
Log.Warning("Tried to allocate addon that was already allocated.");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentAddonCount = RaptureAtkUnitManager.Instance()->AllLoadedUnitsList.Count;
|
||||
if (currentAddonCount >= 200) {
|
||||
Log.Warning($"WARNING: Current Addon Count is approaching hard limits ({currentAddonCount}/250). Please ensure custom Addons are not being used as overlays.");
|
||||
}
|
||||
|
||||
if (currentAddonCount >= 225) {
|
||||
Log.Error($"ERROR: Current Addon Count is too high. Aborting allocation ({currentAddonCount}/250).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (InternalName.Length is 0) {
|
||||
throw new NullReferenceException("InternalName is empty, this is not allowed.");
|
||||
}
|
||||
|
||||
Log.Verbose($"[{InternalName}] Allocating NativeAddon");
|
||||
|
||||
if (!IsOverlayAddon) {
|
||||
InitializeCloseCallback();
|
||||
}
|
||||
|
||||
InternalAddon = NativeMemoryHelper.Create<AtkUnitBase>();
|
||||
|
||||
RegisterVirtualTable();
|
||||
|
||||
RootNode = new ResNode {
|
||||
NodeId = 1,
|
||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.Fill | NodeFlags.Focusable | NodeFlags.EmitsEvents,
|
||||
IsAddonRootNode = true,
|
||||
};
|
||||
|
||||
if (!IsOverlayAddon) {
|
||||
WindowNode = CreateWindowNode?.Invoke() ?? new WindowNode();
|
||||
WindowNode.NodeId = 2;
|
||||
}
|
||||
|
||||
InternalAddon->NameString = InternalName;
|
||||
|
||||
InternalAddon->ShowSoundEffectId = (short)OpenWindowSoundEffectId;
|
||||
|
||||
UpdateFlags();
|
||||
}
|
||||
|
||||
private void InitializeAddon() {
|
||||
var widgetInfo = NativeMemoryHelper.UiAlloc<AtkUldWidgetInfo>(1, 16);
|
||||
widgetInfo->Id = 1;
|
||||
widgetInfo->NodeCount = 0;
|
||||
widgetInfo->NodeList = null;
|
||||
widgetInfo->WidgetAlignment = new AtkWidgetAlignment {
|
||||
AlignmentType = AlignmentType.Center,
|
||||
X = 50.0f,
|
||||
Y = 50.0f,
|
||||
};
|
||||
|
||||
InternalAddon->UldManager.Objects = (AtkUldObjectInfo*)widgetInfo;
|
||||
InternalAddon->UldManager.ObjectCount = 1;
|
||||
InternalAddon->UldManager.ResourceFlags |= AtkUldManagerResourceFlag.ArraysAllocated;
|
||||
|
||||
InternalAddon->RootNode = RootNode;
|
||||
InternalAddon->UldManager.AddNodeToObjectList(RootNode);
|
||||
|
||||
LoadTimeline();
|
||||
|
||||
InternalAddon->UldManager.UpdateDrawNodeList();
|
||||
InternalAddon->UldManager.LoadedState = AtkLoadState.Loaded;
|
||||
|
||||
if (!IsOverlayAddon && WindowNode is not null) {
|
||||
WindowNode.AttachNode(this, NodePosition.AsFirstChild);
|
||||
InternalAddon->WindowNode = WindowNode;
|
||||
InternalAddon->UldManager.AddNodeToObjectList(WindowNode);
|
||||
}
|
||||
|
||||
// UldManager finished loading the uld
|
||||
InternalAddon->Flags198 |= 2 << 0x1C;
|
||||
|
||||
// LoadUldByName called
|
||||
InternalAddon->Flags1A2 |= 4;
|
||||
|
||||
InternalAddon->UpdateCollisionNodeList(false);
|
||||
|
||||
// Set focus node to allow controller nav
|
||||
WindowNode?.WindowHeaderFocusNode.AddNodeFlags(NodeFlags.Focusable);
|
||||
InternalAddon->FocusNode = WindowNode is not null ? WindowNode.WindowHeaderFocusNode : RootNode;
|
||||
|
||||
// Now that we have constructed this instance, track it for auto-dispose
|
||||
CreatedAddons.Add(this);
|
||||
}
|
||||
|
||||
private void SetInitialState() {
|
||||
WindowNode?.SetTitle(Title.ToString(), Subtitle?.ToString() ?? KamiToolKitLibrary.DefaultWindowSubtitle);
|
||||
|
||||
InternalAddon->ShowSoundEffectId = (short)OpenWindowSoundEffectId;
|
||||
|
||||
var addonConfig = LoadAddonConfig();
|
||||
if (addonConfig.Position != Vector2.Zero) {
|
||||
InternalAddon->SetPosition((short)addonConfig.Position.X, (short)addonConfig.Position.Y);
|
||||
}
|
||||
else {
|
||||
var screenSize = new Vector2(AtkStage.Instance()->ScreenSize.Width, AtkStage.Instance()->ScreenSize.Height);
|
||||
var defaultPosition = screenSize / 2.0f - Size / 2.0f;
|
||||
InternalAddon->SetPosition((short)defaultPosition.X, (short)defaultPosition.Y);
|
||||
}
|
||||
|
||||
if (addonConfig.Scale is not 1.0f) {
|
||||
var newScale = Math.Clamp(addonConfig.Scale, 0.25f, 6.0f);
|
||||
|
||||
InternalAddon->SetScale(newScale, true);
|
||||
}
|
||||
|
||||
SetWindowSize(Size);
|
||||
|
||||
if (LastClosePosition != Vector2.Zero && RememberClosePosition) {
|
||||
InternalAddon->SetPosition((short)LastClosePosition.X, (short)LastClosePosition.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public Func<WindowNodeBase>? CreateWindowNode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes and Opens this instance of Addon
|
||||
/// </summary>
|
||||
public void Open() => DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
Log.Verbose($"[{InternalName}] Open Called");
|
||||
|
||||
if (InternalAddon is null) {
|
||||
AllocateAddon();
|
||||
|
||||
if (InternalAddon is not null) {
|
||||
AtkStage.Instance()->RaptureAtkUnitManager->InitializeAddon(InternalAddon, InternalName);
|
||||
InternalAddon->Open((uint)DepthLayer - 1);
|
||||
disposeHandle = GCHandle.Alloc(this);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.Verbose($"[{InternalName}] Already open, skipping call.");
|
||||
}
|
||||
});
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public void DebugOpen() => Open();
|
||||
|
||||
public void Close() {
|
||||
if (InternalAddon is null) return;
|
||||
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
Log.Verbose($"[{InternalName}] Close");
|
||||
|
||||
if (InternalAddon is not null) {
|
||||
InternalAddon->Close(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Toggle() {
|
||||
if (IsOpen) {
|
||||
Close();
|
||||
}
|
||||
else {
|
||||
Open();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddNode(ICollection<NodeBase> nodes) {
|
||||
foreach (var node in nodes) {
|
||||
AddNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddNode(NodeBase? node)
|
||||
=> node?.AttachNode(this);
|
||||
|
||||
private void LoadTimeline() {
|
||||
RootNode.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, 89)
|
||||
.AddLabel(1, 101, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(10, 102, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(20, 103, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(30, 104, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(40, 105, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(50, 106, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(60, 107, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(70, 108, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(80, 109, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user