mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-23 16:27:44 +01:00
Compare commits
39 commits
62b9c1f2a1
...
e4eca842d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4eca842d3 | ||
|
|
c79fa96505 | ||
|
|
596af24e95 | ||
|
|
ba0cf4c990 | ||
|
|
9a49a9588b | ||
|
|
19a3926051 | ||
|
|
4937a2f4bd | ||
|
|
78ed4a2b01 | ||
|
|
72dc094b57 | ||
|
|
9d0879148c | ||
|
|
778c82fad2 | ||
|
|
7f2ed9adb6 | ||
|
|
53b94caeb7 | ||
|
|
d1dc81318a | ||
|
|
a48eead85e | ||
|
|
d1bed3ebc5 | ||
|
|
23e7c164d8 | ||
|
|
8a9b47c7a4 | ||
|
|
520e3ea028 | ||
|
|
dd70c5b8ee | ||
|
|
2b2f628096 | ||
|
|
497e61f699 | ||
|
|
5cc327c5f9 | ||
|
|
af8b61f08a | ||
|
|
700aaa4a5d | ||
|
|
69caffeb97 | ||
|
|
a06c0e3ed2 | ||
|
|
880add5ab3 | ||
|
|
193d321103 | ||
|
|
6e8efabc3b | ||
|
|
68c02caf37 | ||
|
|
878080d660 | ||
|
|
986dfa04d0 | ||
|
|
3746c47a84 | ||
|
|
c4dd75bdda | ||
|
|
5905afdf10 | ||
|
|
62fdd2c60d | ||
|
|
ba159f8c5f | ||
|
|
6ade5b21cf |
28 changed files with 2911 additions and 346 deletions
107
Dalamud.Test/Pipes/DalamudUriTests.cs
Normal file
107
Dalamud.Test/Pipes/DalamudUriTests.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Networking.Pipes;
|
||||
using Xunit;
|
||||
|
||||
namespace Dalamud.Test.Pipes
|
||||
{
|
||||
public class DalamudUriTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("https://www.google.com/", false)]
|
||||
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)]
|
||||
public void ValidatesScheme(string uri, bool valid)
|
||||
{
|
||||
Action act = () => { _ = DalamudUri.FromUri(uri); };
|
||||
|
||||
var ex = Record.Exception(act);
|
||||
if (valid)
|
||||
{
|
||||
Assert.Null(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.NotNull(ex);
|
||||
Assert.IsType<ArgumentOutOfRangeException>(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")]
|
||||
[InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")]
|
||||
[InlineData("dalamud://Test", "test")]
|
||||
public void ExtractsNamespace(string uri, string expectedNamespace)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
Assert.Equal(expectedNamespace, dalamudUri.Namespace);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")]
|
||||
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")]
|
||||
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
|
||||
[InlineData("dalamud://foo/bar", "/bar")]
|
||||
[InlineData("dalamud://foo/bar/", "/bar/")]
|
||||
[InlineData("dalamud://foo/", "/")]
|
||||
public void ExtractsPath(string uri, string expectedPath)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
Assert.Equal(expectedPath, dalamudUri.Path);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
|
||||
[InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar", "/bar")]
|
||||
[InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")]
|
||||
[InlineData("dalamud://foo/bar/", "/bar/")]
|
||||
[InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")]
|
||||
[InlineData("dalamud://foo/?cow=moo", "/?cow=moo")]
|
||||
[InlineData("dalamud://foo/", "/")]
|
||||
public void ExtractsData(string uri, string expectedData)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
|
||||
Assert.Equal(expectedData, dalamudUri.Data);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar", 0)]
|
||||
[InlineData("dalamud://foo/bar?cow=moo", 1)]
|
||||
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)]
|
||||
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)]
|
||||
public void ExtractsQueryParams(string uri, int queryCount)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
Assert.Equal(queryCount, dalamudUri.QueryParams.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)]
|
||||
[InlineData("dalamud://foo/bar/baz/qux/meh", 5)]
|
||||
[InlineData("dalamud://foo/bar/baz/qux", 4)]
|
||||
[InlineData("dalamud://foo/bar/baz", 3)]
|
||||
[InlineData("dalamud://foo/bar/", 2)]
|
||||
[InlineData("dalamud://foo/bar", 2)]
|
||||
public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false)
|
||||
{
|
||||
var dalamudUri = DalamudUri.FromUri(uri);
|
||||
var segments = dalamudUri.Segments;
|
||||
|
||||
// First segment must always be `/`
|
||||
Assert.Equal("/", segments[0]);
|
||||
|
||||
Assert.Equal(segmentCount, segments.Length);
|
||||
|
||||
if (finalSegmentEndsWithSlash)
|
||||
{
|
||||
Assert.EndsWith("/", segments.Last());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<PropertyGroup Label="Feature">
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<DalamudVersion>13.0.0.8</DalamudVersion>
|
||||
<DalamudVersion>13.0.0.9</DalamudVersion>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
<FileVersion>$(DalamudVersion)</FileVersion>
|
||||
|
|
@ -81,6 +81,7 @@
|
|||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="sqlite-net-pcl" />
|
||||
<PackageReference Include="StreamJsonRpc" />
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
|
|
|||
|
|
@ -63,47 +63,37 @@ public interface IAetheryteEntry
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class representing an aetheryte entry available to the game.
|
||||
/// This struct represents an aetheryte entry available to the game.
|
||||
/// </summary>
|
||||
internal sealed class AetheryteEntry : IAetheryteEntry
|
||||
/// <param name="data">Data read from the Aetheryte List.</param>
|
||||
internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry
|
||||
{
|
||||
private readonly TeleportInfo data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AetheryteEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="data">Data read from the Aetheryte List.</param>
|
||||
internal AetheryteEntry(TeleportInfo data)
|
||||
{
|
||||
this.data = data;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public uint AetheryteId => data.AetheryteId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint AetheryteId => this.data.AetheryteId;
|
||||
public uint TerritoryId => data.TerritoryId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint TerritoryId => this.data.TerritoryId;
|
||||
public byte SubIndex => data.SubIndex;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte SubIndex => this.data.SubIndex;
|
||||
public byte Ward => data.Ward;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Ward => this.data.Ward;
|
||||
public byte Plot => data.Plot;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte Plot => this.data.Plot;
|
||||
public uint GilCost => data.GilCost;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint GilCost => this.data.GilCost;
|
||||
public bool IsFavourite => data.IsFavourite;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsFavourite => this.data.IsFavourite;
|
||||
public bool IsSharedHouse => data.IsSharedHouse;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSharedHouse => this.data.IsSharedHouse;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsApartment => this.data.IsApartment;
|
||||
public bool IsApartment => data.IsApartment;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);
|
||||
|
|
|
|||
|
|
@ -87,10 +87,7 @@ internal sealed partial class AetheryteList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IAetheryteEntry> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -98,4 +95,30 @@ internal sealed partial class AetheryteList
|
|||
{
|
||||
return this.GetEnumerator();
|
||||
}
|
||||
|
||||
private struct Enumerator(AetheryteList aetheryteList) : IEnumerator<IAetheryteEntry>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IAetheryteEntry Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == aetheryteList.Length) return false;
|
||||
this.Current = aetheryteList[this.index];
|
||||
this.index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ using Dalamud.Plugin.Services;
|
|||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
|
||||
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
|
||||
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Buddy;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -21,7 +24,7 @@ namespace Dalamud.Game.ClientState.Buddy;
|
|||
#pragma warning restore SA1015
|
||||
internal sealed partial class BuddyList : IServiceType, IBuddyList
|
||||
{
|
||||
private const uint InvalidObjectID = 0xE0000000;
|
||||
private const uint InvalidEntityId = 0xE0000000;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ClientState clientState = Service<ClientState>.Get();
|
||||
|
|
@ -69,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy;
|
||||
private unsafe CSBuddy* BuddyListStruct => &UIState.Instance()->Buddy;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IBuddyMember? this[int index]
|
||||
|
|
@ -82,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetCompanionBuddyMemberAddress()
|
||||
public unsafe nint GetCompanionBuddyMemberAddress()
|
||||
{
|
||||
return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion;
|
||||
return (nint)this.BuddyListStruct->CompanionInfo.Companion;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetPetBuddyMemberAddress()
|
||||
public unsafe nint GetPetBuddyMemberAddress()
|
||||
{
|
||||
return (IntPtr)this.BuddyListStruct->PetInfo.Pet;
|
||||
return (nint)this.BuddyListStruct->PetInfo.Pet;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
|
||||
public unsafe nint GetBattleBuddyMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= 3)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
|
||||
return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
|
||||
public unsafe IBuddyMember? CreateBuddyMemberReference(nint address)
|
||||
{
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
if (this.clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
var buddy = new BuddyMember(address);
|
||||
if (buddy.ObjectId == InvalidObjectID)
|
||||
var buddy = new BuddyMember((CSBuddyMember*)address);
|
||||
if (buddy.EntityId == InvalidEntityId)
|
||||
return null;
|
||||
|
||||
return buddy;
|
||||
|
|
@ -130,12 +133,35 @@ internal sealed partial class BuddyList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IBuddyMember> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(BuddyList buddyList) : IEnumerator<IBuddyMember>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IBuddyMember Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == buddyList.Length) return false;
|
||||
this.Current = buddyList[this.index];
|
||||
this.index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Buddy;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// </summary>
|
||||
public interface IBuddyMember
|
||||
public interface IBuddyMember : IEquatable<IBuddyMember>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the buddy in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object ID of this buddy.
|
||||
|
|
@ -67,42 +71,34 @@ public interface IBuddyMember
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
|
||||
/// </summary>
|
||||
internal unsafe class BuddyMember : IBuddyMember
|
||||
/// <param name="ptr">A pointer to the BuddyMember.</param>
|
||||
internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuddyMember"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Buddy address.</param>
|
||||
internal BuddyMember(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IntPtr Address { get; }
|
||||
public uint ObjectId => this.EntityId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint ObjectId => this.Struct->EntityId;
|
||||
public uint EntityId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint EntityId => this.Struct->EntityId;
|
||||
public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
|
||||
public uint CurrentHP => ptr->CurrentHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint CurrentHP => this.Struct->CurrentHealth;
|
||||
public uint MaxHP => ptr->MaxHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint MaxHP => this.Struct->MaxHealth;
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint DataID => this.Struct->DataId;
|
||||
public uint DataID => ptr->DataId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
|
||||
|
|
@ -113,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember
|
|||
/// <inheritdoc />
|
||||
public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID);
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
|
||||
public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IBuddyMember? other)
|
||||
{
|
||||
return this.EntityId == other.EntityId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is BuddyMember fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.EntityId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Data;
|
||||
|
|
@ -6,10 +7,12 @@ using Dalamud.Memory;
|
|||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Fates;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing an fate entry that can be seen in the current area.
|
||||
/// Interface representing a fate entry that can be seen in the current area.
|
||||
/// </summary>
|
||||
public interface IFate : IEquatable<IFate>
|
||||
{
|
||||
|
|
@ -111,133 +114,96 @@ public interface IFate : IEquatable<IFate>
|
|||
/// <summary>
|
||||
/// Gets the address of this Fate in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents an FFXIV Fate.
|
||||
/// This struct represents a Fate.
|
||||
/// </summary>
|
||||
internal unsafe partial class Fate
|
||||
/// <param name="ptr">A pointer to the FateContext.</param>
|
||||
internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Fate"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of this fate in memory.</param>
|
||||
internal Fate(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IntPtr Address { get; }
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address;
|
||||
|
||||
public static bool operator ==(Fate fate1, Fate fate2)
|
||||
{
|
||||
if (fate1 is null || fate2 is null)
|
||||
return Equals(fate1, fate2);
|
||||
|
||||
return fate1.Equals(fate2);
|
||||
}
|
||||
|
||||
public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this Fate is still valid in memory.
|
||||
/// </summary>
|
||||
/// <param name="fate">The fate to check.</param>
|
||||
/// <returns>True or false.</returns>
|
||||
public static bool IsValid(Fate fate)
|
||||
{
|
||||
var clientState = Service<ClientState>.GetNullable();
|
||||
|
||||
if (fate == null || clientState == null)
|
||||
return false;
|
||||
|
||||
if (clientState.LocalContentId == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this actor is still valid in memory.
|
||||
/// </summary>
|
||||
/// <returns>True or false.</returns>
|
||||
public bool IsValid() => IsValid(this);
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IEquatable<IFate>.Equals(IFate other) => this.FateId == other?.FateId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => ((IEquatable<IFate>)this).Equals(obj as IFate);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.FateId.GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents an FFXIV Fate.
|
||||
/// </summary>
|
||||
internal unsafe partial class Fate : IFate
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public ushort FateId => this.Struct->FateId;
|
||||
public ushort FateId => ptr->FateId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int StartTimeEpoch => this.Struct->StartTimeEpoch;
|
||||
public int StartTimeEpoch => ptr->StartTimeEpoch;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public short Duration => this.Struct->Duration;
|
||||
public short Duration => ptr->Duration;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name);
|
||||
public SeString Name => MemoryHelper.ReadSeString(&ptr->Name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description);
|
||||
public SeString Description => MemoryHelper.ReadSeString(&ptr->Description);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective);
|
||||
public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FateState State => (FateState)this.Struct->State;
|
||||
public FateState State => (FateState)ptr->State;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte HandInCount => this.Struct->HandInCount;
|
||||
public byte HandInCount => ptr->HandInCount;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Progress => this.Struct->Progress;
|
||||
public byte Progress => ptr->Progress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasBonus => this.Struct->IsBonus;
|
||||
public bool HasBonus => ptr->IsBonus;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint IconId => this.Struct->IconId;
|
||||
public uint IconId => ptr->IconId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Level => this.Struct->Level;
|
||||
public byte Level => ptr->Level;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte MaxLevel => this.Struct->MaxLevel;
|
||||
public byte MaxLevel => ptr->MaxLevel;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Vector3 Position => this.Struct->Location;
|
||||
public Vector3 Position => ptr->Location;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float Radius => this.Struct->Radius;
|
||||
public float Radius => ptr->Radius;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint MapIconId => this.Struct->MapIconId;
|
||||
public uint MapIconId => ptr->MapIconId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the territory this <see cref="Fate"/> is located in.
|
||||
/// </summary>
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId);
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->MapMarkers[0].MapMarkerData.TerritoryTypeId);
|
||||
|
||||
public static bool operator ==(Fate x, Fate y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(Fate x, Fate y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IFate? other)
|
||||
{
|
||||
return this.FateId == other.FateId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is Fate fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.FateId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Dalamud.IoC;
|
|||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
|
||||
using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Fates;
|
||||
|
|
@ -25,7 +26,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr Address => (nint)CSFateManager.Instance();
|
||||
public unsafe nint Address => (nint)CSFateManager.Instance();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe int Length
|
||||
|
|
@ -72,30 +73,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetFateAddress(int index)
|
||||
public unsafe nint GetFateAddress(int index)
|
||||
{
|
||||
if (index >= this.Length)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
var fateManager = CSFateManager.Instance();
|
||||
if (fateManager == null)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)fateManager->Fates[index].Value;
|
||||
return (nint)fateManager->Fates[index].Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFate? CreateFateReference(IntPtr offset)
|
||||
public unsafe IFate? CreateFateReference(IntPtr address)
|
||||
{
|
||||
var clientState = Service<ClientState>.Get();
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
var clientState = Service<ClientState>.Get();
|
||||
if (clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (offset == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
return new Fate(offset);
|
||||
return new Fate((CSFateContext*)address);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,12 +110,35 @@ internal sealed partial class FateTable
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IFate> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(FateTable fateTable) : IEnumerator<IFate>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IFate Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == fateTable.Length) return false;
|
||||
this.Current = fateTable[this.index];
|
||||
this.index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ using Dalamud.Utility;
|
|||
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
|
||||
|
||||
|
|
@ -34,8 +32,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
private readonly ClientState clientState;
|
||||
private readonly CachedEntry[] cachedObjectTable;
|
||||
|
||||
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private unsafe ObjectTable(ClientState clientState)
|
||||
{
|
||||
|
|
@ -47,9 +43,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
this.cachedObjectTable = new CachedEntry[objectTableLength];
|
||||
for (var i = 0; i < this.cachedObjectTable.Length; i++)
|
||||
this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
|
||||
|
||||
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
|
||||
this.frameworkThreadEnumerators[i] = new(this, i);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -239,30 +232,14 @@ internal sealed partial class ObjectTable
|
|||
public IEnumerator<IGameObject> GetEnumerator()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
// If we're on the framework thread, see if there's an already allocated enumerator available for use.
|
||||
foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
|
||||
{
|
||||
if (x is not null)
|
||||
{
|
||||
var t = x;
|
||||
x = null;
|
||||
t.Reset();
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
// No reusable enumerator is available; allocate a new temporary one.
|
||||
return new Enumerator(this, -1);
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator<IGameObject>, IResettable
|
||||
private struct Enumerator(ObjectTable owner) : IEnumerator<IGameObject>
|
||||
{
|
||||
private ObjectTable? owner = owner;
|
||||
|
||||
private int index = -1;
|
||||
|
||||
public IGameObject Current { get; private set; } = null!;
|
||||
|
|
@ -274,7 +251,7 @@ internal sealed partial class ObjectTable
|
|||
if (this.index == objectTableLength)
|
||||
return false;
|
||||
|
||||
var cache = this.owner!.cachedObjectTable.AsSpan();
|
||||
var cache = owner.cachedObjectTable.AsSpan();
|
||||
for (this.index++; this.index < objectTableLength; this.index++)
|
||||
{
|
||||
if (cache[this.index].Update() is { } ao)
|
||||
|
|
@ -291,17 +268,6 @@ internal sealed partial class ObjectTable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.owner is not { } o)
|
||||
return;
|
||||
|
||||
if (slotId != -1)
|
||||
o.frameworkThreadEnumerators[slotId] = this;
|
||||
}
|
||||
|
||||
public bool TryReset()
|
||||
{
|
||||
this.Reset();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using Dalamud.IoC.Internal;
|
|||
using Dalamud.Plugin.Services;
|
||||
|
||||
using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
|
||||
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Party;
|
||||
|
||||
|
|
@ -42,20 +43,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
|
|||
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
|
||||
public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
|
||||
public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
|
||||
public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long PartyId => this.GroupManagerStruct->MainGroup.PartyId;
|
||||
|
||||
private static int PartyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember>();
|
||||
private static int PartyMemberSize { get; } = Marshal.SizeOf<CSPartyMember>();
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
|
||||
private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPartyMember? this[int index]
|
||||
|
|
@ -80,45 +81,45 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr GetPartyMemberAddress(int index)
|
||||
public nint GetPartyMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= GroupLength)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return this.GroupListAddress + (index * PartyMemberSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPartyMember? CreatePartyMemberReference(IntPtr address)
|
||||
public IPartyMember? CreatePartyMemberReference(nint address)
|
||||
{
|
||||
if (this.clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember(address);
|
||||
return new PartyMember((CSPartyMember*)address);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr GetAllianceMemberAddress(int index)
|
||||
public nint GetAllianceMemberAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= AllianceLength)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return this.AllianceListAddress + (index * PartyMemberSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPartyMember? CreateAllianceMemberReference(IntPtr address)
|
||||
public IPartyMember? CreateAllianceMemberReference(nint address)
|
||||
{
|
||||
if (this.clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new PartyMember(address);
|
||||
return new PartyMember((CSPartyMember*)address);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,18 +134,44 @@ internal sealed partial class PartyList
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<IPartyMember> GetEnumerator()
|
||||
{
|
||||
// Normally using Length results in a recursion crash, however we know the party size via ptr.
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
var member = this[i];
|
||||
|
||||
if (member == null)
|
||||
break;
|
||||
|
||||
yield return member;
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private struct Enumerator(PartyList partyList) : IEnumerator<IPartyMember>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IPartyMember Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == partyList.Length) return false;
|
||||
|
||||
for (; this.index < partyList.Length; this.index++)
|
||||
{
|
||||
var partyMember = partyList[this.index];
|
||||
if (partyMember != null)
|
||||
{
|
||||
this.Current = partyMember;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Statuses;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Memory;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Party;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing a party member.
|
||||
/// </summary>
|
||||
public interface IPartyMember
|
||||
public interface IPartyMember : IEquatable<IPartyMember>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of this party member in memory.
|
||||
/// </summary>
|
||||
IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of buffs or debuffs applied to this party member.
|
||||
|
|
@ -108,69 +109,81 @@ public interface IPartyMember
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a party member in the group manager.
|
||||
/// This struct represents a party member in the group manager.
|
||||
/// </summary>
|
||||
internal unsafe class PartyMember : IPartyMember
|
||||
/// <param name="ptr">A pointer to the PartyMember.</param>
|
||||
internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PartyMember"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the party member.</param>
|
||||
internal PartyMember(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr Address { get; }
|
||||
public StatusList Statuses => new(&ptr->StatusManager);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public StatusList Statuses => new(&this.Struct->StatusManager);
|
||||
public Vector3 Position => ptr->Position;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Vector3 Position => this.Struct->Position;
|
||||
public long ContentId => (long)ptr->ContentId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long ContentId => (long)this.Struct->ContentId;
|
||||
public uint ObjectId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint ObjectId => this.Struct->EntityId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint EntityId => this.Struct->EntityId;
|
||||
public uint EntityId => ptr->EntityId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint CurrentHP => this.Struct->CurrentHP;
|
||||
public uint CurrentHP => ptr->CurrentHP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint MaxHP => this.Struct->MaxHP;
|
||||
public uint MaxHP => ptr->MaxHP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort CurrentMP => this.Struct->CurrentMP;
|
||||
public ushort CurrentMP => ptr->CurrentMP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort MaxMP => this.Struct->MaxMP;
|
||||
public ushort MaxMP => ptr->MaxMP;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryType);
|
||||
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->TerritoryType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(this.Struct->HomeWorld);
|
||||
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(ptr->HomeWorld);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SeString Name => SeString.Parse(this.Struct->Name);
|
||||
public SeString Name => SeString.Parse(ptr->Name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Sex => this.Struct->Sex;
|
||||
public byte Sex => ptr->Sex;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(this.Struct->ClassJob);
|
||||
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(ptr->ClassJob);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte Level => this.Struct->Level;
|
||||
public byte Level => ptr->Level;
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;
|
||||
public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(PartyMember x, PartyMember y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IPartyMember? other)
|
||||
{
|
||||
return this.EntityId == other.EntityId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is PartyMember fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.EntityId.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +1,49 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
|
||||
using Lumina.Excel;
|
||||
|
||||
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Statuses;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a status effect an actor is afflicted by.
|
||||
/// Interface representing a status.
|
||||
/// </summary>
|
||||
public unsafe class Status
|
||||
public interface IStatus : IEquatable<IStatus>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Status"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Status address.</param>
|
||||
internal Status(IntPtr address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the status in memory.
|
||||
/// </summary>
|
||||
public IntPtr Address { get; }
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status ID of this status.
|
||||
/// </summary>
|
||||
public uint StatusId => this.Struct->StatusId;
|
||||
uint StatusId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GameData associated with this status.
|
||||
/// </summary>
|
||||
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(this.Struct->StatusId);
|
||||
RowRef<Lumina.Excel.Sheets.Status> GameData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter value of the status.
|
||||
/// </summary>
|
||||
public ushort Param => this.Struct->Param;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stack count of this status.
|
||||
/// Only valid if this is a non-food status.
|
||||
/// </summary>
|
||||
[Obsolete($"Replaced with {nameof(Param)}", true)]
|
||||
public byte StackCount => (byte)this.Struct->Param;
|
||||
ushort Param { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time remaining of this status.
|
||||
/// </summary>
|
||||
public float RemainingTime => this.Struct->RemainingTime;
|
||||
float RemainingTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source ID of this status.
|
||||
/// </summary>
|
||||
public uint SourceId => this.Struct->SourceObject.ObjectId;
|
||||
uint SourceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source actor associated with this status.
|
||||
|
|
@ -63,7 +51,55 @@ public unsafe class Status
|
|||
/// <remarks>
|
||||
/// This iterates the actor table, it should be used with care.
|
||||
/// </remarks>
|
||||
IGameObject? SourceObject { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This struct represents a status effect an actor is afflicted by.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the Status.</param>
|
||||
internal unsafe readonly struct Status(CSStatus* ptr) : IStatus
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint StatusId => ptr->StatusId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(ptr->StatusId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ushort Param => ptr->Param;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float RemainingTime => ptr->RemainingTime;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint SourceId => ptr->SourceObject.ObjectId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IGameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceId);
|
||||
|
||||
private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
|
||||
public static bool operator ==(Status x, Status y) => x.Equals(y);
|
||||
|
||||
public static bool operator !=(Status x, Status y) => !(x == y);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(IStatus? other)
|
||||
{
|
||||
return this.StatusId == other.StatusId && this.SourceId == other.SourceId && this.Param == other.Param && this.RemainingTime == other.RemainingTime;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is Status fate && this.Equals(fate);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.StatusId, this.SourceId, this.Param, this.RemainingTime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Statuses;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -14,7 +16,7 @@ public sealed unsafe partial class StatusList
|
|||
/// Initializes a new instance of the <see cref="StatusList"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address of the status list.</param>
|
||||
internal StatusList(IntPtr address)
|
||||
internal StatusList(nint address)
|
||||
{
|
||||
this.Address = address;
|
||||
}
|
||||
|
|
@ -24,14 +26,14 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="pointer">Pointer to the status list.</param>
|
||||
internal unsafe StatusList(void* pointer)
|
||||
: this((IntPtr)pointer)
|
||||
: this((nint)pointer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the status list in memory.
|
||||
/// </summary>
|
||||
public IntPtr Address { get; }
|
||||
public nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of status effect slots the actor has.
|
||||
|
|
@ -47,7 +49,7 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="index">Status Index.</param>
|
||||
/// <returns>The status at the specified index.</returns>
|
||||
public Status? this[int index]
|
||||
public IStatus? this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
|
|
@ -64,7 +66,7 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="address">The address of the status list in memory.</param>
|
||||
/// <returns>The status object containing the requested data.</returns>
|
||||
public static StatusList? CreateStatusListReference(IntPtr address)
|
||||
public static StatusList? CreateStatusListReference(nint address)
|
||||
{
|
||||
// The use case for CreateStatusListReference and CreateStatusReference to be static is so
|
||||
// fake status lists can be generated. Since they aren't exposed as services, it's either
|
||||
|
|
@ -74,7 +76,7 @@ public sealed unsafe partial class StatusList
|
|||
if (clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new StatusList(address);
|
||||
|
|
@ -85,17 +87,17 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="address">The address of the status effect in memory.</param>
|
||||
/// <returns>The status object containing the requested data.</returns>
|
||||
public static Status? CreateStatusReference(IntPtr address)
|
||||
public static IStatus? CreateStatusReference(nint address)
|
||||
{
|
||||
var clientState = Service<ClientState>.Get();
|
||||
|
||||
if (clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == 0)
|
||||
return null;
|
||||
|
||||
return new Status(address);
|
||||
return new Status((CSStatus*)address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -103,22 +105,22 @@ public sealed unsafe partial class StatusList
|
|||
/// </summary>
|
||||
/// <param name="index">The index of the status.</param>
|
||||
/// <returns>The memory address of the status.</returns>
|
||||
public IntPtr GetStatusAddress(int index)
|
||||
public nint GetStatusAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= this.Length)
|
||||
return IntPtr.Zero;
|
||||
return 0;
|
||||
|
||||
return (IntPtr)Unsafe.AsPointer(ref this.Struct->Status[index]);
|
||||
return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This collection represents the status effects an actor is afflicted by.
|
||||
/// </summary>
|
||||
public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection
|
||||
public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollection
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
int IReadOnlyCollection<Status>.Count => this.Length;
|
||||
int IReadOnlyCollection<IStatus>.Count => this.Length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
int ICollection.Count => this.Length;
|
||||
|
|
@ -130,17 +132,9 @@ public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollectio
|
|||
object ICollection.SyncRoot => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<Status> GetEnumerator()
|
||||
public IEnumerator<IStatus> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Length; i++)
|
||||
{
|
||||
var status = this[i];
|
||||
|
||||
if (status == null || status.StatusId == 0)
|
||||
continue;
|
||||
|
||||
yield return status;
|
||||
}
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -155,4 +149,39 @@ public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollectio
|
|||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
private struct Enumerator(StatusList statusList) : IEnumerator<IStatus>
|
||||
{
|
||||
private int index = 0;
|
||||
|
||||
public IStatus Current { get; private set; }
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == statusList.Length) return false;
|
||||
|
||||
for (; this.index < statusList.Length; this.index++)
|
||||
{
|
||||
var status = statusList[this.index];
|
||||
if (status != null && status.StatusId != 0)
|
||||
{
|
||||
this.Current = status;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Structs;
|
||||
|
||||
/// <summary>
|
||||
/// Native memory representation of a FFXIV status effect.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct StatusEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The effect ID.
|
||||
/// </summary>
|
||||
public short EffectId;
|
||||
|
||||
/// <summary>
|
||||
/// How many stacks are present.
|
||||
/// </summary>
|
||||
public byte StackCount;
|
||||
|
||||
/// <summary>
|
||||
/// Additional parameters.
|
||||
/// </summary>
|
||||
public byte Param;
|
||||
|
||||
/// <summary>
|
||||
/// The duration remaining.
|
||||
/// </summary>
|
||||
public float Duration;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the actor that caused this effect.
|
||||
/// </summary>
|
||||
public int OwnerId;
|
||||
}
|
||||
95
Dalamud/Game/UnlockState/ItemActionType.cs
Normal file
95
Dalamud/Game/UnlockState/ItemActionType.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Dalamud.Game.UnlockState;
|
||||
|
||||
/// <summary>
|
||||
/// Enum for <see cref="ItemAction.Type"/>.
|
||||
/// </summary>
|
||||
internal enum ItemActionType : ushort
|
||||
{
|
||||
/// <summary>
|
||||
/// No item action.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks a companion (minion).
|
||||
/// </summary>
|
||||
Companion = 853,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks a chocobo companion barding.
|
||||
/// </summary>
|
||||
BuddyEquip = 1013,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks a mount.
|
||||
/// </summary>
|
||||
Mount = 1322,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks recipes from a crafting recipe book.
|
||||
/// </summary>
|
||||
SecretRecipeBook = 2136,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks various types of content (e.g. Riding Maps, Blue Mage Totems, Emotes, Hairstyles).
|
||||
/// </summary>
|
||||
UnlockLink = 2633,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks a Triple Triad Card.
|
||||
/// </summary>
|
||||
TripleTriadCard = 3357,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks gathering nodes of a Folklore Tome.
|
||||
/// </summary>
|
||||
FolkloreTome = 4107,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks an Orchestrion Roll.
|
||||
/// </summary>
|
||||
OrchestrionRoll = 25183,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks portrait designs.
|
||||
/// </summary>
|
||||
FramersKit = 29459,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks Bozjan Field Notes.
|
||||
/// </summary>
|
||||
/// <remarks> These are server-side but are cached client-side. </remarks>
|
||||
FieldNotes = 19743,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks an Ornament (fashion accessory).
|
||||
/// </summary>
|
||||
Ornament = 20086,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks Glasses.
|
||||
/// </summary>
|
||||
Glasses = 37312,
|
||||
|
||||
/// <summary>
|
||||
/// Company Seal Vouchers, which convert the item into Company Seals when used.
|
||||
/// </summary>
|
||||
CompanySealVouchers = 41120,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks Occult Records in Occult Crescent.
|
||||
/// </summary>
|
||||
OccultRecords = 43141,
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks Phantom Jobs in Occult Crescent.
|
||||
/// </summary>
|
||||
SoulShards = 43142,
|
||||
|
||||
/// <summary>
|
||||
/// Star Contributor Certificate, which grants the Star Contributor status in Cosmic Exploration.
|
||||
/// </summary>
|
||||
StarContributorCertificate = 45189,
|
||||
}
|
||||
283
Dalamud/Game/UnlockState/RecipeData.cs
Normal file
283
Dalamud/Game/UnlockState/RecipeData.cs
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
using System.Linq;
|
||||
|
||||
using CommunityToolkit.HighPerformance;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.Gui;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Dalamud.Game.UnlockState;
|
||||
|
||||
/// <summary>
|
||||
/// Represents recipe-related data for all crafting classes.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class RecipeData : IInternalDisposableService
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DataManager dataManager = Service<DataManager>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameGui gameGui = Service<GameGui>.Get();
|
||||
|
||||
private readonly ushort[] craftTypeLevels;
|
||||
private readonly byte[] unlockedNoteBookDivisionsCount;
|
||||
private readonly byte[] unlockedSecretNoteBookDivisionsCount;
|
||||
private readonly ushort[,] noteBookDivisionIds;
|
||||
private byte[]? cachedUnlockedSecretRecipeBooks;
|
||||
private byte[]? cachedUnlockLinks;
|
||||
private byte[]? cachedCompletedQuests;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecipeData"/> class.
|
||||
/// </summary>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public RecipeData()
|
||||
{
|
||||
var numCraftTypes = this.dataManager.GetExcelSheet<CraftType>().Count();
|
||||
var numSecretNotBookDivisions = this.dataManager.GetExcelSheet<NotebookDivision>().Count(row => row.RowId is >= 1000 and < 2000);
|
||||
|
||||
this.unlockedNoteBookDivisionsCount = new byte[numCraftTypes];
|
||||
this.unlockedSecretNoteBookDivisionsCount = new byte[numCraftTypes];
|
||||
this.noteBookDivisionIds = new ushort[numCraftTypes, numSecretNotBookDivisions];
|
||||
|
||||
this.craftTypeLevels = new ushort[numCraftTypes];
|
||||
|
||||
this.clientState.Login += this.Update;
|
||||
this.clientState.Logout += this.OnLogout;
|
||||
this.clientState.LevelChanged += this.OnlevelChanged;
|
||||
this.gameGui.AgentUpdate += this.OnAgentUpdate;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.clientState.Login -= this.Update;
|
||||
this.clientState.Logout -= this.OnLogout;
|
||||
this.clientState.LevelChanged -= this.OnlevelChanged;
|
||||
this.gameGui.AgentUpdate -= this.OnAgentUpdate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Recipe is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Recipe row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
public bool IsRecipeUnlocked(Recipe row)
|
||||
{
|
||||
// E8 ?? ?? ?? ?? 48 63 76 (2025.09.04)
|
||||
var division = row.RecipeNotebookList.RowId != 0 && row.RecipeNotebookList.IsValid
|
||||
? (row.RecipeNotebookList.RowId - 1000) / 8 + 1000
|
||||
: ((uint)row.RecipeLevelTable.Value.ClassJobLevel - 1) / 5;
|
||||
|
||||
// E8 ?? ?? ?? ?? 33 ED 84 C0 75 (2025.09.04)
|
||||
foreach (var craftTypeRow in this.dataManager.GetExcelSheet<CraftType>())
|
||||
{
|
||||
var craftType = (byte)craftTypeRow.RowId;
|
||||
|
||||
if (division < this.unlockedNoteBookDivisionsCount[craftType])
|
||||
return true;
|
||||
|
||||
if (this.unlockedNoteBookDivisionsCount[craftType] == 0)
|
||||
continue;
|
||||
|
||||
if (division is 5000 or 5001)
|
||||
return true;
|
||||
|
||||
if (division < 1000)
|
||||
continue;
|
||||
|
||||
if (this.unlockedSecretNoteBookDivisionsCount[craftType] == 0)
|
||||
continue;
|
||||
|
||||
if (this.noteBookDivisionIds.GetRowSpan(craftType).Contains((ushort)division))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnLogout(int type, int code)
|
||||
{
|
||||
this.cachedUnlockedSecretRecipeBooks = null;
|
||||
this.cachedUnlockLinks = null;
|
||||
this.cachedCompletedQuests = null;
|
||||
}
|
||||
|
||||
private void OnlevelChanged(uint classJobId, uint level)
|
||||
{
|
||||
if (this.dataManager.GetExcelSheet<ClassJob>().TryGetRow(classJobId, out var classJobRow) &&
|
||||
classJobRow.ClassJobCategory.RowId == 33) // Crafter
|
||||
{
|
||||
this.Update();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag)
|
||||
{
|
||||
if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate))
|
||||
this.Update();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// based on Client::Game::UI::RecipeNote.InitializeStructs
|
||||
|
||||
if (!this.clientState.IsLoggedIn || !this.NeedsUpdate())
|
||||
return;
|
||||
|
||||
Array.Clear(this.unlockedNoteBookDivisionsCount, 0, this.unlockedNoteBookDivisionsCount.Length);
|
||||
Array.Clear(this.unlockedSecretNoteBookDivisionsCount, 0, this.unlockedSecretNoteBookDivisionsCount.Length);
|
||||
Array.Clear(this.noteBookDivisionIds, 0, this.noteBookDivisionIds.Length);
|
||||
|
||||
foreach (var craftTypeRow in this.dataManager.GetExcelSheet<CraftType>())
|
||||
{
|
||||
var craftType = (byte)craftTypeRow.RowId;
|
||||
var craftTypeLevel = RecipeNote.Instance()->GetCraftTypeLevel(craftType);
|
||||
if (craftTypeLevel == 0)
|
||||
continue;
|
||||
|
||||
var noteBookDivisionIndex = -1;
|
||||
|
||||
foreach (var noteBookDivisionRow in this.dataManager.GetExcelSheet<NotebookDivision>())
|
||||
{
|
||||
if (noteBookDivisionRow.RowId < 1000)
|
||||
{
|
||||
if (craftTypeLevel >= noteBookDivisionRow.CraftOpeningLevel)
|
||||
this.unlockedNoteBookDivisionsCount[craftType]++;
|
||||
}
|
||||
else if (noteBookDivisionRow.RowId < 2000)
|
||||
{
|
||||
noteBookDivisionIndex++;
|
||||
|
||||
// For future Lumina.Excel update, replace with:
|
||||
// if (!notebookDivisionRow.AllowedCraftTypes[craftType])
|
||||
// continue;
|
||||
|
||||
switch (craftTypeRow.RowId)
|
||||
{
|
||||
case 0 when !noteBookDivisionRow.CRPCraft: continue;
|
||||
case 1 when !noteBookDivisionRow.BSMCraft: continue;
|
||||
case 2 when !noteBookDivisionRow.ARMCraft: continue;
|
||||
case 3 when !noteBookDivisionRow.GSMCraft: continue;
|
||||
case 4 when !noteBookDivisionRow.LTWCraft: continue;
|
||||
case 5 when !noteBookDivisionRow.WVRCraft: continue;
|
||||
case 6 when !noteBookDivisionRow.ALCCraft: continue;
|
||||
case 7 when !noteBookDivisionRow.CULCraft: continue;
|
||||
}
|
||||
|
||||
if (noteBookDivisionRow.GatheringOpeningLevel != byte.MaxValue)
|
||||
continue;
|
||||
|
||||
// For future Lumina.Excel update, replace with:
|
||||
// if (notebookDivisionRow.RequiresSecretRecipeBookGroupUnlock)
|
||||
if (noteBookDivisionRow.Unknown1)
|
||||
{
|
||||
var secretRecipeBookUnlocked = false;
|
||||
|
||||
// For future Lumina.Excel update, iterate over notebookDivisionRow.SecretRecipeBookGroups
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
// For future Lumina.Excel update, replace with:
|
||||
// if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid)
|
||||
// continue;
|
||||
var secretRecipeBookGroupRowId = i switch
|
||||
{
|
||||
0 => noteBookDivisionRow.Unknown2,
|
||||
1 => noteBookDivisionRow.Unknown2,
|
||||
_ => default,
|
||||
};
|
||||
|
||||
if (secretRecipeBookGroupRowId == 0)
|
||||
continue;
|
||||
|
||||
if (!this.dataManager.GetExcelSheet<SecretRecipeBookGroup>().TryGetRow(secretRecipeBookGroupRowId, out var secretRecipeBookGroupRow))
|
||||
continue;
|
||||
|
||||
// For future Lumina.Excel update, replace with:
|
||||
// var bitIndex = secretRecipeBookGroup.Value.UnlockBitIndex[craftType];
|
||||
|
||||
var bitIndex = craftType switch
|
||||
{
|
||||
0 => secretRecipeBookGroupRow.Unknown0,
|
||||
1 => secretRecipeBookGroupRow.Unknown1,
|
||||
2 => secretRecipeBookGroupRow.Unknown2,
|
||||
3 => secretRecipeBookGroupRow.Unknown3,
|
||||
4 => secretRecipeBookGroupRow.Unknown4,
|
||||
5 => secretRecipeBookGroupRow.Unknown5,
|
||||
6 => secretRecipeBookGroupRow.Unknown6,
|
||||
7 => secretRecipeBookGroupRow.Unknown7,
|
||||
_ => default,
|
||||
};
|
||||
|
||||
if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get(bitIndex))
|
||||
{
|
||||
secretRecipeBookUnlocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (noteBookDivisionRow.CraftOpeningLevel > craftTypeLevel && !secretRecipeBookUnlocked)
|
||||
continue;
|
||||
}
|
||||
else if (craftTypeLevel < noteBookDivisionRow.CraftOpeningLevel)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (noteBookDivisionRow.QuestUnlock.RowId != 0 && !UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(noteBookDivisionRow.QuestUnlock.RowId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this.unlockedSecretNoteBookDivisionsCount[craftType]++;
|
||||
this.noteBookDivisionIds[craftType, noteBookDivisionIndex] = (ushort)noteBookDivisionRow.RowId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool NeedsUpdate()
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
foreach (var craftTypeRow in this.dataManager.GetExcelSheet<CraftType>())
|
||||
{
|
||||
var craftType = (byte)craftTypeRow.RowId;
|
||||
var craftTypeLevel = RecipeNote.Instance()->GetCraftTypeLevel(craftType);
|
||||
|
||||
if (this.craftTypeLevels[craftType] != craftTypeLevel)
|
||||
{
|
||||
this.craftTypeLevels[craftType] = craftTypeLevel;
|
||||
changed |= true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.cachedUnlockedSecretRecipeBooks == null || !PlayerState.Instance()->UnlockedSecretRecipeBooks.SequenceEqual(this.cachedUnlockedSecretRecipeBooks))
|
||||
{
|
||||
this.cachedUnlockedSecretRecipeBooks = PlayerState.Instance()->UnlockedSecretRecipeBooks.ToArray();
|
||||
changed |= true;
|
||||
}
|
||||
|
||||
if (this.cachedUnlockLinks == null || !UIState.Instance()->UnlockLinks.SequenceEqual(this.cachedUnlockLinks))
|
||||
{
|
||||
this.cachedUnlockLinks = UIState.Instance()->UnlockLinks.ToArray();
|
||||
changed |= true;
|
||||
}
|
||||
|
||||
if (this.cachedCompletedQuests == null || !QuestManager.Instance()->CompletedQuests.SequenceEqual(this.cachedCompletedQuests))
|
||||
{
|
||||
this.cachedCompletedQuests = QuestManager.Instance()->CompletedQuests.ToArray();
|
||||
changed |= true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
858
Dalamud/Game/UnlockState/UnlockState.cs
Normal file
858
Dalamud/Game/UnlockState/UnlockState.cs
Normal file
|
|
@ -0,0 +1,858 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.Exd;
|
||||
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
using ActionSheet = Lumina.Excel.Sheets.Action;
|
||||
using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent;
|
||||
using PublicContentSheet = Lumina.Excel.Sheets.PublicContent;
|
||||
|
||||
namespace Dalamud.Game.UnlockState;
|
||||
|
||||
#pragma warning disable UnlockState
|
||||
|
||||
/// <summary>
|
||||
/// This class provides unlock state of various content in the game.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
|
||||
{
|
||||
private static readonly ModuleLog Log = new(nameof(UnlockState));
|
||||
|
||||
private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = [];
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DataManager dataManager = Service<DataManager>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameGui gameGui = Service<GameGui>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly RecipeData recipeData = Service<RecipeData>.Get();
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private UnlockState()
|
||||
{
|
||||
this.clientState.Login += this.OnLogin;
|
||||
this.clientState.Logout += this.OnLogout;
|
||||
this.gameGui.AgentUpdate += this.OnAgentUpdate;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IUnlockState.UnlockDelegate Unlock;
|
||||
|
||||
private bool IsLoaded => PlayerState.Instance()->IsLoaded;
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.clientState.Login -= this.OnLogin;
|
||||
this.clientState.Logout -= this.OnLogout;
|
||||
this.gameGui.AgentUpdate -= this.OnAgentUpdate;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsActionUnlocked(ActionSheet row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAetherCurrentUnlocked(AetherCurrent row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return PlayerState.Instance()->IsAetherCurrentUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return PlayerState.Instance()->IsAetherCurrentZoneComplete(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAozActionUnlocked(AozAction row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
if (row.RowId == 0 || !row.Action.IsValid)
|
||||
return false;
|
||||
|
||||
return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(row.Action.Value.UnlockLink.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerBgUnlocked(BannerBg row)
|
||||
{
|
||||
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerConditionUnlocked(BannerCondition row)
|
||||
{
|
||||
if (row.RowId == 0)
|
||||
return false;
|
||||
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
var rowPtr = ExdModule.GetBannerConditionByIndex(row.RowId);
|
||||
if (rowPtr == null)
|
||||
return false;
|
||||
|
||||
return ExdModule.GetBannerConditionUnlockState(rowPtr) == 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerDecorationUnlocked(BannerDecoration row)
|
||||
{
|
||||
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerFacialUnlocked(BannerFacial row)
|
||||
{
|
||||
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerFrameUnlocked(BannerFrame row)
|
||||
{
|
||||
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerTimelineUnlocked(BannerTimeline row)
|
||||
{
|
||||
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBuddyActionUnlocked(BuddyAction row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.UnlockLink);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBuddyEquipUnlocked(BuddyEquip row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row)
|
||||
{
|
||||
return row.IsPurchasable && this.IsUnlockLinkUnlocked(row.UnlockLink);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCompanionUnlocked(Companion row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return UIState.Instance()->IsCompanionUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCraftActionUnlocked(CraftAction row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.QuestRequirement.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.UnlockLink);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsEmoteUnlocked(Emote row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.UnlockLink);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.Unknown26);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsEmjCostumeUnlocked(EmjCostume row)
|
||||
{
|
||||
return this.dataManager.GetExcelSheet<EmjVoiceNpc>().TryGetRow(row.RowId, out var emjVoiceNpcRow)
|
||||
&& this.IsEmjVoiceNpcUnlocked(emjVoiceNpcRow)
|
||||
&& QuestManager.IsQuestComplete(row.Unknown1);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGeneralActionUnlocked(GeneralAction row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.UnlockLink);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGlassesUnlocked(Glasses row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsHowToUnlocked(HowTo row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return UIState.Instance()->IsHowToUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInstanceContentUnlocked(InstanceContentSheet row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return UIState.IsInstanceContentUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe bool IsItemUnlocked(Item row)
|
||||
{
|
||||
if (row.ItemAction.RowId == 0)
|
||||
return false;
|
||||
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
// To avoid the ExdModule.GetItemRowById call, which can return null if the excel page
|
||||
// is not loaded, we're going to imitate the IsItemActionUnlocked call first:
|
||||
switch ((ItemActionType)row.ItemAction.Value.Type)
|
||||
{
|
||||
case ItemActionType.Companion:
|
||||
return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]);
|
||||
|
||||
case ItemActionType.BuddyEquip:
|
||||
return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]);
|
||||
|
||||
case ItemActionType.Mount:
|
||||
return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]);
|
||||
|
||||
case ItemActionType.SecretRecipeBook:
|
||||
return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]);
|
||||
|
||||
case ItemActionType.UnlockLink:
|
||||
case ItemActionType.OccultRecords:
|
||||
return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]);
|
||||
|
||||
case ItemActionType.TripleTriadCard when row.AdditionalData.Is<TripleTriadCard>():
|
||||
return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId);
|
||||
|
||||
case ItemActionType.FolkloreTome:
|
||||
return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]);
|
||||
|
||||
case ItemActionType.OrchestrionRoll when row.AdditionalData.Is<Orchestrion>():
|
||||
return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId);
|
||||
|
||||
case ItemActionType.FramersKit:
|
||||
return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId);
|
||||
|
||||
case ItemActionType.Ornament:
|
||||
return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]);
|
||||
|
||||
case ItemActionType.Glasses:
|
||||
return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId);
|
||||
|
||||
case ItemActionType.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null:
|
||||
var supportJobId = (byte)row.ItemAction.Value.Data[0];
|
||||
return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0;
|
||||
|
||||
case ItemActionType.CompanySealVouchers:
|
||||
return false;
|
||||
}
|
||||
|
||||
var nativeRow = ExdModule.GetItemRowById(row.RowId);
|
||||
return nativeRow != null && UIState.Instance()->IsItemActionUnlocked(nativeRow) == 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsMcGuffinUnlocked(McGuffin row)
|
||||
{
|
||||
return PlayerState.Instance()->IsMcGuffinUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsMJILandmarkUnlocked(MJILandmark row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.UnlockLink);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsMKDLoreUnlocked(MKDLore row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.Unknown2);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsMountUnlocked(Mount row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return PlayerState.Instance()->IsMountUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsNotebookDivisionUnlocked(NotebookDivision row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.QuestUnlock.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsOrchestrionUnlocked(Orchestrion row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsOrnamentUnlocked(Ornament row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return PlayerState.Instance()->IsOrnamentUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsPerformUnlocked(Perform row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked((uint)row.UnlockLink);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsPublicContentUnlocked(PublicContentSheet row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return UIState.IsPublicContentUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRecipeUnlocked(Recipe row)
|
||||
{
|
||||
return this.recipeData.IsRecipeUnlocked(row);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsTraitUnlocked(Trait row)
|
||||
{
|
||||
return this.IsUnlockLinkUnlocked(row.Quest.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsTripleTriadCardUnlocked(TripleTriadCard row)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.RowId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsItemUnlockable(Item row)
|
||||
{
|
||||
if (row.ItemAction.RowId == 0)
|
||||
return false;
|
||||
|
||||
return (ItemActionType)row.ItemAction.Value.Type
|
||||
is ItemActionType.Companion
|
||||
or ItemActionType.BuddyEquip
|
||||
or ItemActionType.Mount
|
||||
or ItemActionType.SecretRecipeBook
|
||||
or ItemActionType.UnlockLink
|
||||
or ItemActionType.TripleTriadCard
|
||||
or ItemActionType.FolkloreTome
|
||||
or ItemActionType.OrchestrionRoll
|
||||
or ItemActionType.FramersKit
|
||||
or ItemActionType.Ornament
|
||||
or ItemActionType.Glasses
|
||||
or ItemActionType.OccultRecords
|
||||
or ItemActionType.SoulShards;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRowRefUnlocked<T>(RowRef<T> rowRef) where T : struct, IExcelRow<T>
|
||||
{
|
||||
return this.IsRowRefUnlocked((RowRef)rowRef);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRowRefUnlocked(RowRef rowRef)
|
||||
{
|
||||
if (!this.IsLoaded || rowRef.IsUntyped)
|
||||
return false;
|
||||
|
||||
if (rowRef.TryGetValue<ActionSheet>(out var actionRow))
|
||||
return this.IsActionUnlocked(actionRow);
|
||||
|
||||
if (rowRef.TryGetValue<AetherCurrent>(out var aetherCurrentRow))
|
||||
return this.IsAetherCurrentUnlocked(aetherCurrentRow);
|
||||
|
||||
if (rowRef.TryGetValue<AetherCurrentCompFlgSet>(out var aetherCurrentCompFlgSetRow))
|
||||
return this.IsAetherCurrentCompFlgSetUnlocked(aetherCurrentCompFlgSetRow);
|
||||
|
||||
if (rowRef.TryGetValue<AozAction>(out var aozActionRow))
|
||||
return this.IsAozActionUnlocked(aozActionRow);
|
||||
|
||||
if (rowRef.TryGetValue<BannerBg>(out var bannerBgRow))
|
||||
return this.IsBannerBgUnlocked(bannerBgRow);
|
||||
|
||||
if (rowRef.TryGetValue<BannerCondition>(out var bannerConditionRow))
|
||||
return this.IsBannerConditionUnlocked(bannerConditionRow);
|
||||
|
||||
if (rowRef.TryGetValue<BannerDecoration>(out var bannerDecorationRow))
|
||||
return this.IsBannerDecorationUnlocked(bannerDecorationRow);
|
||||
|
||||
if (rowRef.TryGetValue<BannerFacial>(out var bannerFacialRow))
|
||||
return this.IsBannerFacialUnlocked(bannerFacialRow);
|
||||
|
||||
if (rowRef.TryGetValue<BannerFrame>(out var bannerFrameRow))
|
||||
return this.IsBannerFrameUnlocked(bannerFrameRow);
|
||||
|
||||
if (rowRef.TryGetValue<BannerTimeline>(out var bannerTimelineRow))
|
||||
return this.IsBannerTimelineUnlocked(bannerTimelineRow);
|
||||
|
||||
if (rowRef.TryGetValue<BuddyAction>(out var buddyActionRow))
|
||||
return this.IsBuddyActionUnlocked(buddyActionRow);
|
||||
|
||||
if (rowRef.TryGetValue<BuddyEquip>(out var buddyEquipRow))
|
||||
return this.IsBuddyEquipUnlocked(buddyEquipRow);
|
||||
|
||||
if (rowRef.TryGetValue<CSBonusContentType>(out var csBonusContentTypeRow))
|
||||
return this.IsCSBonusContentTypeUnlocked(csBonusContentTypeRow);
|
||||
|
||||
if (rowRef.TryGetValue<CharaMakeCustomize>(out var charaMakeCustomizeRow))
|
||||
return this.IsCharaMakeCustomizeUnlocked(charaMakeCustomizeRow);
|
||||
|
||||
if (rowRef.TryGetValue<ChocoboTaxiStand>(out var chocoboTaxiStandRow))
|
||||
return this.IsChocoboTaxiStandUnlocked(chocoboTaxiStandRow);
|
||||
|
||||
if (rowRef.TryGetValue<Companion>(out var companionRow))
|
||||
return this.IsCompanionUnlocked(companionRow);
|
||||
|
||||
if (rowRef.TryGetValue<CraftAction>(out var craftActionRow))
|
||||
return this.IsCraftActionUnlocked(craftActionRow);
|
||||
|
||||
if (rowRef.TryGetValue<Emote>(out var emoteRow))
|
||||
return this.IsEmoteUnlocked(emoteRow);
|
||||
|
||||
if (rowRef.TryGetValue<GeneralAction>(out var generalActionRow))
|
||||
return this.IsGeneralActionUnlocked(generalActionRow);
|
||||
|
||||
if (rowRef.TryGetValue<Glasses>(out var glassesRow))
|
||||
return this.IsGlassesUnlocked(glassesRow);
|
||||
|
||||
if (rowRef.TryGetValue<HowTo>(out var howToRow))
|
||||
return this.IsHowToUnlocked(howToRow);
|
||||
|
||||
if (rowRef.TryGetValue<InstanceContentSheet>(out var instanceContentRow))
|
||||
return this.IsInstanceContentUnlocked(instanceContentRow);
|
||||
|
||||
if (rowRef.TryGetValue<Item>(out var itemRow))
|
||||
return this.IsItemUnlocked(itemRow);
|
||||
|
||||
if (rowRef.TryGetValue<MJILandmark>(out var mjiLandmarkRow))
|
||||
return this.IsMJILandmarkUnlocked(mjiLandmarkRow);
|
||||
|
||||
if (rowRef.TryGetValue<MKDLore>(out var mkdLoreRow))
|
||||
return this.IsMKDLoreUnlocked(mkdLoreRow);
|
||||
|
||||
if (rowRef.TryGetValue<McGuffin>(out var mcGuffinRow))
|
||||
return this.IsMcGuffinUnlocked(mcGuffinRow);
|
||||
|
||||
if (rowRef.TryGetValue<Mount>(out var mountRow))
|
||||
return this.IsMountUnlocked(mountRow);
|
||||
|
||||
if (rowRef.TryGetValue<NotebookDivision>(out var notebookDivisionRow))
|
||||
return this.IsNotebookDivisionUnlocked(notebookDivisionRow);
|
||||
|
||||
if (rowRef.TryGetValue<Orchestrion>(out var orchestrionRow))
|
||||
return this.IsOrchestrionUnlocked(orchestrionRow);
|
||||
|
||||
if (rowRef.TryGetValue<Ornament>(out var ornamentRow))
|
||||
return this.IsOrnamentUnlocked(ornamentRow);
|
||||
|
||||
if (rowRef.TryGetValue<Perform>(out var performRow))
|
||||
return this.IsPerformUnlocked(performRow);
|
||||
|
||||
if (rowRef.TryGetValue<PublicContentSheet>(out var publicContentRow))
|
||||
return this.IsPublicContentUnlocked(publicContentRow);
|
||||
|
||||
if (rowRef.TryGetValue<Recipe>(out var recipeRow))
|
||||
return this.IsRecipeUnlocked(recipeRow);
|
||||
|
||||
if (rowRef.TryGetValue<SecretRecipeBook>(out var secretRecipeBookRow))
|
||||
return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow);
|
||||
|
||||
if (rowRef.TryGetValue<Trait>(out var traitRow))
|
||||
return this.IsTraitUnlocked(traitRow);
|
||||
|
||||
if (rowRef.TryGetValue<TripleTriadCard>(out var tripleTriadCardRow))
|
||||
return this.IsTripleTriadCardUnlocked(tripleTriadCardRow);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsUnlockLinkUnlocked(ushort unlockLink)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
if (unlockLink == 0)
|
||||
return false;
|
||||
|
||||
return UIState.Instance()->IsUnlockLinkUnlocked(unlockLink);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsUnlockLinkUnlocked(uint unlockLink)
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return false;
|
||||
|
||||
if (unlockLink == 0)
|
||||
return false;
|
||||
|
||||
return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(unlockLink);
|
||||
}
|
||||
|
||||
private void OnLogin()
|
||||
{
|
||||
this.Update();
|
||||
}
|
||||
|
||||
private void OnLogout(int type, int code)
|
||||
{
|
||||
this.cachedUnlockedRowIds.Clear();
|
||||
}
|
||||
|
||||
private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag)
|
||||
{
|
||||
if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate))
|
||||
this.Update();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!this.IsLoaded)
|
||||
return;
|
||||
|
||||
this.UpdateUnlocksForSheet<ActionSheet>();
|
||||
this.UpdateUnlocksForSheet<AetherCurrent>();
|
||||
this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>();
|
||||
this.UpdateUnlocksForSheet<AozAction>();
|
||||
this.UpdateUnlocksForSheet<BannerBg>();
|
||||
this.UpdateUnlocksForSheet<BannerCondition>();
|
||||
this.UpdateUnlocksForSheet<BannerDecoration>();
|
||||
this.UpdateUnlocksForSheet<BannerFacial>();
|
||||
this.UpdateUnlocksForSheet<BannerFrame>();
|
||||
this.UpdateUnlocksForSheet<BannerTimeline>();
|
||||
this.UpdateUnlocksForSheet<BuddyAction>();
|
||||
this.UpdateUnlocksForSheet<BuddyEquip>();
|
||||
this.UpdateUnlocksForSheet<CSBonusContentType>();
|
||||
this.UpdateUnlocksForSheet<CharaMakeCustomize>();
|
||||
this.UpdateUnlocksForSheet<ChocoboTaxi>();
|
||||
this.UpdateUnlocksForSheet<Companion>();
|
||||
this.UpdateUnlocksForSheet<CraftAction>();
|
||||
this.UpdateUnlocksForSheet<EmjVoiceNpc>();
|
||||
this.UpdateUnlocksForSheet<Emote>();
|
||||
this.UpdateUnlocksForSheet<GeneralAction>();
|
||||
this.UpdateUnlocksForSheet<Glasses>();
|
||||
this.UpdateUnlocksForSheet<HowTo>();
|
||||
this.UpdateUnlocksForSheet<InstanceContentSheet>();
|
||||
this.UpdateUnlocksForSheet<Item>();
|
||||
this.UpdateUnlocksForSheet<MJILandmark>();
|
||||
this.UpdateUnlocksForSheet<MKDLore>();
|
||||
this.UpdateUnlocksForSheet<McGuffin>();
|
||||
this.UpdateUnlocksForSheet<Mount>();
|
||||
this.UpdateUnlocksForSheet<NotebookDivision>();
|
||||
this.UpdateUnlocksForSheet<Orchestrion>();
|
||||
this.UpdateUnlocksForSheet<Ornament>();
|
||||
this.UpdateUnlocksForSheet<Perform>();
|
||||
this.UpdateUnlocksForSheet<PublicContentSheet>();
|
||||
this.UpdateUnlocksForSheet<Recipe>();
|
||||
this.UpdateUnlocksForSheet<SecretRecipeBook>();
|
||||
this.UpdateUnlocksForSheet<Trait>();
|
||||
this.UpdateUnlocksForSheet<TripleTriadCard>();
|
||||
|
||||
// Not implemented:
|
||||
// - DescriptionPage: quite complex
|
||||
// - QuestAcceptAdditionCondition: ignored
|
||||
|
||||
// For some other day:
|
||||
// - FishingSpot
|
||||
// - Spearfishing
|
||||
// - Adventure (Sightseeing)
|
||||
// - MinerFolkloreTome
|
||||
// - BotanistFolkloreTome
|
||||
// - FishingFolkloreTome
|
||||
// - VVD or is that unlocked via quest?
|
||||
// - VVDNotebookContents?
|
||||
// - FramersKit (is that just an Item?)
|
||||
// - ... more?
|
||||
|
||||
// Subrow sheets, which are incompatible with the current Unlock event, since RowRef doesn't carry the SubrowId:
|
||||
// - EmjCostume
|
||||
|
||||
// Probably not happening, because it requires fetching data from server:
|
||||
// - Achievements
|
||||
// - Titles
|
||||
// - Bozjan Field Notes
|
||||
// - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0
|
||||
}
|
||||
|
||||
private void UpdateUnlocksForSheet<T>() where T : struct, IExcelRow<T>
|
||||
{
|
||||
var unlockedRowIds = this.cachedUnlockedRowIds.GetOrAdd(typeof(T), _ => []);
|
||||
|
||||
foreach (var row in this.dataManager.GetExcelSheet<T>())
|
||||
{
|
||||
if (unlockedRowIds.Contains(row.RowId))
|
||||
continue;
|
||||
|
||||
var rowRef = LuminaUtils.CreateRef<T>(row.RowId);
|
||||
|
||||
if (!this.IsRowRefUnlocked(rowRef))
|
||||
continue;
|
||||
|
||||
unlockedRowIds.Add(row.RowId);
|
||||
|
||||
Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}");
|
||||
|
||||
foreach (var action in Delegate.EnumerateInvocationList(this.Unlock))
|
||||
{
|
||||
try
|
||||
{
|
||||
action((RowRef)rowRef);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Exception during raise of {handler}", action.Method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-scoped version of a <see cref="UnlockState"/> service.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[ServiceManager.ScopedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IUnlockState>]
|
||||
#pragma warning restore SA1015
|
||||
internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockState
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly UnlockState unlockStateService = Service<UnlockState>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnlockStatePluginScoped"/> class.
|
||||
/// </summary>
|
||||
internal UnlockStatePluginScoped()
|
||||
{
|
||||
this.unlockStateService.Unlock += this.UnlockForward;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IUnlockState.UnlockDelegate? Unlock;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAetherCurrentUnlocked(AetherCurrent row) => this.unlockStateService.IsAetherCurrentUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAozActionUnlocked(AozAction row) => this.unlockStateService.IsAozActionUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerBgUnlocked(BannerBg row) => this.unlockStateService.IsBannerBgUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerConditionUnlocked(BannerCondition row) => this.unlockStateService.IsBannerConditionUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerDecorationUnlocked(BannerDecoration row) => this.unlockStateService.IsBannerDecorationUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerFacialUnlocked(BannerFacial row) => this.unlockStateService.IsBannerFacialUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerFrameUnlocked(BannerFrame row) => this.unlockStateService.IsBannerFrameUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBannerTimelineUnlocked(BannerTimeline row) => this.unlockStateService.IsBannerTimelineUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBuddyActionUnlocked(BuddyAction row) => this.unlockStateService.IsBuddyActionUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBuddyEquipUnlocked(BuddyEquip row) => this.unlockStateService.IsBuddyEquipUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row) => this.unlockStateService.IsCharaMakeCustomizeUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row) => this.unlockStateService.IsChocoboTaxiStandUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCompanionUnlocked(Companion row) => this.unlockStateService.IsCompanionUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCraftActionUnlocked(CraftAction row) => this.unlockStateService.IsCraftActionUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row) => this.unlockStateService.IsCSBonusContentTypeUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsEmoteUnlocked(Emote row) => this.unlockStateService.IsEmoteUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) => this.unlockStateService.IsEmjVoiceNpcUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsEmjCostumeUnlocked(EmjCostume row) => this.unlockStateService.IsEmjCostumeUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGeneralActionUnlocked(GeneralAction row) => this.unlockStateService.IsGeneralActionUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGlassesUnlocked(Glasses row) => this.unlockStateService.IsGlassesUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsHowToUnlocked(HowTo row) => this.unlockStateService.IsHowToUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInstanceContentUnlocked(InstanceContentSheet row) => this.unlockStateService.IsInstanceContentUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsItemUnlockable(Item row) => this.unlockStateService.IsItemUnlockable(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsItemUnlocked(Item row) => this.unlockStateService.IsItemUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsMJILandmarkUnlocked(MJILandmark row) => this.unlockStateService.IsMJILandmarkUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsMKDLoreUnlocked(MKDLore row) => this.unlockStateService.IsMKDLoreUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsMountUnlocked(Mount row) => this.unlockStateService.IsMountUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsNotebookDivisionUnlocked(NotebookDivision row) => this.unlockStateService.IsNotebookDivisionUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsOrchestrionUnlocked(Orchestrion row) => this.unlockStateService.IsOrchestrionUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsOrnamentUnlocked(Ornament row) => this.unlockStateService.IsOrnamentUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsPerformUnlocked(Perform row) => this.unlockStateService.IsPerformUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsPublicContentUnlocked(PublicContentSheet row) => this.unlockStateService.IsPublicContentUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRecipeUnlocked(Recipe row) => this.unlockStateService.IsRecipeUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRowRefUnlocked(RowRef rowRef) => this.unlockStateService.IsRowRefUnlocked(rowRef);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRowRefUnlocked<T>(RowRef<T> rowRef) where T : struct, IExcelRow<T> => this.unlockStateService.IsRowRefUnlocked(rowRef);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsTripleTriadCardUnlocked(TripleTriadCard row) => this.unlockStateService.IsTripleTriadCardUnlocked(row);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsUnlockLinkUnlocked(uint unlockLink) => this.unlockStateService.IsUnlockLinkUnlocked(unlockLink);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsUnlockLinkUnlocked(ushort unlockLink) => this.unlockStateService.IsUnlockLinkUnlocked(unlockLink);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.unlockStateService.Unlock -= this.UnlockForward;
|
||||
}
|
||||
|
||||
private void UnlockForward(RowRef rowRef) => this.Unlock?.Invoke(rowRef);
|
||||
}
|
||||
56
Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
Normal file
56
Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System.Linq;
|
||||
|
||||
using Dalamud.Console;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Networking.Pipes.Internal;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
#pragma warning disable DAL_RPC
|
||||
|
||||
namespace Dalamud.Networking.Pipes.Api;
|
||||
|
||||
/// <inheritdoc cref="IPluginLinkHandler" />
|
||||
[PluginInterface]
|
||||
[ServiceManager.ScopedService]
|
||||
[ResolveVia<IPluginLinkHandler>]
|
||||
public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler
|
||||
{
|
||||
private readonly LinkHandlerService linkHandler;
|
||||
private readonly LocalPlugin localPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginLinkHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localPlugin">The plugin to bind this service to.</param>
|
||||
/// <param name="linkHandler">The central link handler.</param>
|
||||
internal PluginLinkHandler(LocalPlugin localPlugin, LinkHandlerService linkHandler)
|
||||
{
|
||||
this.linkHandler = linkHandler;
|
||||
this.localPlugin = localPlugin;
|
||||
|
||||
this.linkHandler.Register("plugin", this.HandleUri);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IPluginLinkHandler.PluginUriReceived? OnUriReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
{
|
||||
this.OnUriReceived = null;
|
||||
this.linkHandler.Unregister("plugin", this.HandleUri);
|
||||
}
|
||||
|
||||
private void HandleUri(DalamudUri uri)
|
||||
{
|
||||
var target = uri.Path.Split("/").FirstOrDefault();
|
||||
var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName);
|
||||
if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.OnUriReceived?.Invoke(uri);
|
||||
}
|
||||
}
|
||||
102
Dalamud/Networking/Pipes/DalamudUri.cs
Normal file
102
Dalamud/Networking/Pipes/DalamudUri.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Web;
|
||||
|
||||
namespace Dalamud.Networking.Pipes;
|
||||
|
||||
/// <summary>
|
||||
/// A Dalamud Uri, in the format:
|
||||
/// <code>dalamud://{NAMESPACE}/{ARBITRARY}</code>
|
||||
/// </summary>
|
||||
public record DalamudUri
|
||||
{
|
||||
private readonly Uri rawUri;
|
||||
|
||||
private DalamudUri(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != "dalamud")
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(uri), "URI must be of scheme dalamud.");
|
||||
}
|
||||
|
||||
this.rawUri = uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the namespace that this URI should be routed to. Generally a high level component like "PluginInstaller".
|
||||
/// </summary>
|
||||
public string Namespace => this.rawUri.Authority;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw (untargeted) path and query params for this URI.
|
||||
/// </summary>
|
||||
public string Data =>
|
||||
this.rawUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw (untargeted) path for this URI.
|
||||
/// </summary>
|
||||
public string Path => this.rawUri.AbsolutePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of segments based on the provided Data element.
|
||||
/// </summary>
|
||||
public string[] Segments => this.GetDataSegments();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw query parameters for this URI, if any.
|
||||
/// </summary>
|
||||
public string Query => this.rawUri.Query;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the query params (as a parsed NameValueCollection) in this URI.
|
||||
/// </summary>
|
||||
public NameValueCollection QueryParams => HttpUtility.ParseQueryString(this.Query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fragment (if one is specified) in this URI.
|
||||
/// </summary>
|
||||
public string Fragment => this.rawUri.Fragment;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this.rawUri.ToString();
|
||||
|
||||
private string[] GetDataSegments()
|
||||
{
|
||||
// reimplementation of the System.URI#Segments, under MIT license.
|
||||
var path = this.Path;
|
||||
|
||||
var segments = new List<string>();
|
||||
var current = 0;
|
||||
while (current < path.Length)
|
||||
{
|
||||
var next = path.IndexOf('/', current);
|
||||
if (next == -1)
|
||||
{
|
||||
next = path.Length - 1;
|
||||
}
|
||||
|
||||
segments.Add(path.Substring(current, (next - current) + 1));
|
||||
current = next + 1;
|
||||
}
|
||||
|
||||
return segments.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a DalamudURI from a given URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to convert to a Dalamud URI.</param>
|
||||
/// <returns>Returns a DalamudUri.</returns>
|
||||
public static DalamudUri FromUri(Uri uri)
|
||||
{
|
||||
return new DalamudUri(uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a DalamudURI from a URI in string format.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to convert to a Dalamud URI.</param>
|
||||
/// <returns>Returns a DalamudUri.</returns>
|
||||
public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri));
|
||||
}
|
||||
121
Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
Normal file
121
Dalamud/Networking/Pipes/Internal/ClientHelloService.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Networking.Pipes.Rpc;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Dalamud.Networking.Pipes.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// A minimal service to respond with information about this client.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed class ClientHelloService : IInternalDisposableService
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientHelloService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rpcHostService">Injected host service.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public ClientHelloService(RpcHostService rpcHostService)
|
||||
{
|
||||
rpcHostService.AddMethod("hello", this.HandleHello);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a hello request.
|
||||
/// </summary>
|
||||
/// <param name="request">.</param>
|
||||
/// <returns>Respond with information.</returns>
|
||||
public async Task<ClientHelloResponse> HandleHello(ClientHelloRequest request)
|
||||
{
|
||||
var dalamud = await Service<Dalamud>.GetAsync();
|
||||
|
||||
return new ClientHelloResponse
|
||||
{
|
||||
ApiVersion = "1.0",
|
||||
DalamudVersion = Util.GetScmVersion(),
|
||||
GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown",
|
||||
ClientIdentifier = await this.GetClientIdentifier(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
{
|
||||
}
|
||||
|
||||
private async Task<string> GetClientIdentifier()
|
||||
{
|
||||
var framework = await Service<Framework>.GetAsync();
|
||||
var clientState = await Service<ClientState>.GetAsync();
|
||||
var dataManager = await Service<DataManager>.GetAsync();
|
||||
|
||||
var clientIdentifier = $"FFXIV Process ${Environment.ProcessId}";
|
||||
|
||||
await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (clientState.IsLoggedIn)
|
||||
{
|
||||
var player = clientState.LocalPlayer;
|
||||
if (player != null)
|
||||
{
|
||||
var world = dataManager.GetExcelSheet<World>().GetRow(player.HomeWorld.RowId);
|
||||
clientIdentifier = $"Logged in as {player.Name.TextValue} @ {world.Name.ExtractText()}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
clientIdentifier = "On login screen";
|
||||
}
|
||||
});
|
||||
|
||||
return clientIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A request from a client to say hello.
|
||||
/// </summary>
|
||||
internal record ClientHelloRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the API version this client is expecting.
|
||||
/// </summary>
|
||||
public string ApiVersion { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user agent of the client.
|
||||
/// </summary>
|
||||
public string UserAgent { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A response from Dalamud to a hello request.
|
||||
/// </summary>
|
||||
internal record ClientHelloResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the API version this server has offered.
|
||||
/// </summary>
|
||||
public string? ApiVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current Dalamud version.
|
||||
/// </summary>
|
||||
public string? DalamudVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current game version.
|
||||
/// </summary>
|
||||
public string? GameVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an identifier for this client.
|
||||
/// </summary>
|
||||
public string? ClientIdentifier { get; init; }
|
||||
}
|
||||
107
Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
Normal file
107
Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Networking.Pipes.Rpc;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Networking.Pipes.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// A service responsible for handling Dalamud URIs and dispatching them accordingly.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class LinkHandlerService : IInternalDisposableService
|
||||
{
|
||||
private readonly ModuleLog log = new("LinkHandler");
|
||||
|
||||
// key: namespace (e.g. "plugin" or "PluginInstaller") -> list of handlers
|
||||
private readonly ConcurrentDictionary<string, List<Action<DalamudUri>>> handlers
|
||||
= new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinkHandlerService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="rpcHostService">The injected RPC host service.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public LinkHandlerService(RpcHostService rpcHostService)
|
||||
{
|
||||
rpcHostService.AddMethod("handleLink", this.HandleLinkCall);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a handler for a namespace. All URIs with this namespace will be dispatched to the handler.
|
||||
/// </summary>
|
||||
/// <param name="ns">The namespace to use for this subscription.</param>
|
||||
/// <param name="handler">The command handler.</param>
|
||||
public void Register(string ns, Action<DalamudUri> handler)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ns))
|
||||
throw new ArgumentNullException(nameof(ns));
|
||||
|
||||
var list = this.handlers.GetOrAdd(ns, _ => []);
|
||||
lock (list)
|
||||
{
|
||||
list.Add(handler);
|
||||
}
|
||||
|
||||
this.log.Verbose("Registered handler for {Namespace}", ns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister a handler.
|
||||
/// </summary>
|
||||
/// <param name="ns">The namespace to use for this subscription.</param>
|
||||
/// <param name="handler">The command handler.</param>
|
||||
public void Unregister(string ns, Action<DalamudUri> handler)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ns))
|
||||
return;
|
||||
|
||||
if (!this.handlers.TryGetValue(ns, out var list))
|
||||
return;
|
||||
|
||||
list.RemoveAll(x => x == handler);
|
||||
|
||||
if (list.Count == 0)
|
||||
this.handlers.TryRemove(ns, out _);
|
||||
|
||||
this.log.Verbose("Unregistered handler for {Namespace}", ns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch a URI to matching handlers.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to parse and dispatch.</param>
|
||||
public void Dispatch(DalamudUri uri)
|
||||
{
|
||||
this.log.Information("Received URI: {Uri}", uri.ToString());
|
||||
|
||||
var ns = uri.Namespace;
|
||||
if (!this.handlers.TryGetValue(ns, out var actions))
|
||||
return;
|
||||
|
||||
foreach (var h in actions)
|
||||
{
|
||||
h.InvokeSafely(uri);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The RPC-invokable link handler.
|
||||
/// </summary>
|
||||
/// <param name="uri">A plain-text URI to parse.</param>
|
||||
public void HandleLinkCall(string uri)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri))
|
||||
return;
|
||||
|
||||
var du = DalamudUri.FromUri(uri);
|
||||
this.Dispatch(du);
|
||||
}
|
||||
}
|
||||
167
Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
Normal file
167
Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Pipes;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Networking.Pipes.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc.
|
||||
/// </summary>
|
||||
internal class PipeRpcHost : IDisposable
|
||||
{
|
||||
private readonly ModuleLog log = new("RPC/Host");
|
||||
|
||||
private readonly RpcServiceRegistry registry = new();
|
||||
private readonly CancellationTokenSource cts = new();
|
||||
private readonly ConcurrentDictionary<Guid, RpcConnection> sessions = new();
|
||||
private Task? acceptLoopTask;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PipeRpcHost"/> class.
|
||||
/// </summary>
|
||||
/// <param name="pipeName">The pipe name to create.</param>
|
||||
public PipeRpcHost(string? pipeName = null)
|
||||
{
|
||||
// Default pipe name based on current process ID for uniqueness per Dalamud instance.
|
||||
this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the named pipe this RPC host is using.
|
||||
/// </summary>
|
||||
public string PipeName { get; }
|
||||
|
||||
/// <summary>Adds a local object exposing RPC methods callable by clients.</summary>
|
||||
/// <param name="service">An arbitrary service object that will be introspected to add to RPC.</param>
|
||||
public void AddService(object service) => this.registry.AddService(service);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a standalone JSON-RPC method callable by clients.
|
||||
/// </summary>
|
||||
/// <param name="name">The name to add.</param>
|
||||
/// <param name="handler">The delegate that acts as the handler.</param>
|
||||
public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler);
|
||||
|
||||
/// <summary>Starts accepting client connections.</summary>
|
||||
public void Start()
|
||||
{
|
||||
if (this.acceptLoopTask != null) return;
|
||||
this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
/// <summary>Invoke an RPC request on a specific client expecting a result.</summary>
|
||||
/// <param name="clientId">The client ID to invoke.</param>
|
||||
/// <param name="method">The method to invoke.</param>
|
||||
/// <param name="arguments">Any arguments to invoke.</param>
|
||||
/// <returns>An optional return based on the specified RPC.</returns>
|
||||
/// <typeparam name="T">The expected response type.</typeparam>
|
||||
public Task<T> InvokeClientAsync<T>(Guid clientId, string method, params object[] arguments)
|
||||
{
|
||||
if (!this.sessions.TryGetValue(clientId, out var session))
|
||||
throw new KeyNotFoundException($"No client {clientId}");
|
||||
|
||||
return session.Rpc.InvokeAsync<T>(method, arguments);
|
||||
}
|
||||
|
||||
/// <summary>Send a notification to all connected clients (no response expected).</summary>
|
||||
/// <param name="method">The method name to broadcast.</param>
|
||||
/// <param name="arguments">The arguments to broadcast.</param>
|
||||
/// <returns>Returns a Task when completed.</returns>
|
||||
public Task BroadcastNotifyAsync(string method, params object[] arguments)
|
||||
{
|
||||
var list = this.sessions.Values;
|
||||
var tasks = new List<Task>(list.Count);
|
||||
foreach (var s in list)
|
||||
{
|
||||
tasks.Add(s.Rpc.NotifyAsync(method, arguments));
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of connected client IDs.
|
||||
/// </summary>
|
||||
/// <returns>Connected client IDs.</returns>
|
||||
public IReadOnlyCollection<Guid> GetClientIds() => this.sessions.Keys.AsReadOnlyCollection();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.cts.Cancel();
|
||||
this.acceptLoopTask?.Wait(1000);
|
||||
|
||||
foreach (var kv in this.sessions)
|
||||
{
|
||||
kv.Value.Dispose();
|
||||
}
|
||||
|
||||
this.sessions.Clear();
|
||||
this.cts.Dispose();
|
||||
this.log.Information("PipeRpcHost disposed ({Pipe})", this.PipeName);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private PipeSecurity BuildPipeSecurity()
|
||||
{
|
||||
var ps = new PipeSecurity();
|
||||
ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User!, PipeAccessRights.FullControl, AccessControlType.Allow));
|
||||
|
||||
return ps;
|
||||
}
|
||||
|
||||
private async Task AcceptLoopAsync()
|
||||
{
|
||||
this.log.Information("PipeRpcHost starting on pipe {Pipe}", this.PipeName);
|
||||
var token = this.cts.Token;
|
||||
var security = this.BuildPipeSecurity();
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
NamedPipeServerStream? server = null;
|
||||
try
|
||||
{
|
||||
server = NamedPipeServerStreamAcl.Create(
|
||||
this.PipeName,
|
||||
PipeDirection.InOut,
|
||||
NamedPipeServerStream.MaxAllowedServerInstances,
|
||||
PipeTransmissionMode.Message,
|
||||
PipeOptions.Asynchronous,
|
||||
65536,
|
||||
65536,
|
||||
security);
|
||||
|
||||
await server.WaitForConnectionAsync(token).ConfigureAwait(false);
|
||||
|
||||
var session = new RpcConnection(server, this.registry);
|
||||
this.sessions.TryAdd(session.Id, session);
|
||||
|
||||
this.log.Debug("RPC connection created: {Id}", session.Id);
|
||||
|
||||
_ = session.Completion.ContinueWith(t =>
|
||||
{
|
||||
this.sessions.TryRemove(session.Id, out _);
|
||||
this.log.Debug("RPC connection removed: {Id}", session.Id);
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
server?.Dispose();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
server?.Dispose();
|
||||
this.log.Error(ex, "Error in pipe accept loop");
|
||||
await Task.Delay(500, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
Dalamud/Networking/Pipes/Rpc/RpcConnection.cs
Normal file
92
Dalamud/Networking/Pipes/Rpc/RpcConnection.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using System.IO.Pipes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Serilog;
|
||||
using StreamJsonRpc;
|
||||
|
||||
namespace Dalamud.Networking.Pipes.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// A single RPC client session connected via named pipe.
|
||||
/// </summary>
|
||||
internal class RpcConnection : IDisposable
|
||||
{
|
||||
private readonly NamedPipeServerStream pipe;
|
||||
private readonly RpcServiceRegistry registry;
|
||||
private readonly CancellationTokenSource cts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RpcConnection"/> class.
|
||||
/// </summary>
|
||||
/// <param name="pipe">The named pipe that this connection will handle.</param>
|
||||
/// <param name="registry">A registry of RPC services.</param>
|
||||
public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry)
|
||||
{
|
||||
this.Id = Guid.CreateVersion7();
|
||||
this.pipe = pipe;
|
||||
this.registry = registry;
|
||||
|
||||
var formatter = new JsonMessageFormatter();
|
||||
var handler = new HeaderDelimitedMessageHandler(pipe, pipe, formatter);
|
||||
|
||||
this.Rpc = new JsonRpc(handler);
|
||||
this.Rpc.AllowModificationWhileListening = true;
|
||||
this.Rpc.Disconnected += this.OnDisconnected;
|
||||
this.registry.Attach(this.Rpc);
|
||||
|
||||
this.Rpc.StartListening();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GUID for this connection.
|
||||
/// </summary>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JsonRpc instance for this connection.
|
||||
/// </summary>
|
||||
public JsonRpc Rpc { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a task that's called on RPC completion.
|
||||
/// </summary>
|
||||
public Task Completion => this.Rpc.Completion;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this.cts.IsCancellationRequested)
|
||||
{
|
||||
this.cts.Cancel();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.Rpc.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error disposing JsonRpc for client {Id}", this.Id);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.pipe.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error disposing pipe for client {Id}", this.Id);
|
||||
}
|
||||
|
||||
this.cts.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void OnDisconnected(object? sender, JsonRpcDisconnectedEventArgs e)
|
||||
{
|
||||
Log.Debug("RPC client {Id} disconnected: {Reason}", this.Id, e.Description);
|
||||
this.registry.Detach(this.Rpc);
|
||||
this.Dispose();
|
||||
}
|
||||
}
|
||||
49
Dalamud/Networking/Pipes/Rpc/RpcHostService.cs
Normal file
49
Dalamud/Networking/Pipes/Rpc/RpcHostService.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using Dalamud.Logging.Internal;
|
||||
|
||||
namespace Dalamud.Networking.Pipes.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// The Dalamud service repsonsible for hosting the RPC.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class RpcHostService : IServiceType, IInternalDisposableService
|
||||
{
|
||||
private readonly ModuleLog log = new("RPC");
|
||||
private readonly PipeRpcHost host;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RpcHostService"/> class.
|
||||
/// </summary>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public RpcHostService()
|
||||
{
|
||||
this.host = new PipeRpcHost();
|
||||
this.host.Start();
|
||||
|
||||
this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RPC host to drill down.
|
||||
/// </summary>
|
||||
public PipeRpcHost Host => this.host;
|
||||
|
||||
/// <summary>
|
||||
/// Add a new service Object to the RPC host.
|
||||
/// </summary>
|
||||
/// <param name="service">The object to add.</param>
|
||||
public void AddService(object service) => this.host.AddService(service);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new standalone method to the RPC host.
|
||||
/// </summary>
|
||||
/// <param name="name">The method name to add.</param>
|
||||
/// <param name="handler">The handler to add.</param>
|
||||
public void AddMethod(string name, Delegate handler) => this.host.AddMethod(name, handler);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
{
|
||||
this.host.Dispose();
|
||||
}
|
||||
}
|
||||
85
Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs
Normal file
85
Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
using StreamJsonRpc;
|
||||
|
||||
namespace Dalamud.Networking.Pipes.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session.
|
||||
/// New sessions get all previously registered targets; newly added targets are attached to all active sessions.
|
||||
/// </summary>
|
||||
internal class RpcServiceRegistry
|
||||
{
|
||||
private readonly Lock sync = new();
|
||||
private readonly List<object> targets = [];
|
||||
private readonly List<(string Name, Delegate Handler)> methods = [];
|
||||
private readonly List<JsonRpc> activeRpcs = [];
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new local RPC target object. Its public JSON-RPC methods become callable by clients.
|
||||
/// Adds <paramref name="service"/> to the registry and attaches it to all active RPC sessions.
|
||||
/// </summary>
|
||||
/// <param name="service">The service instance containing JSON-RPC callable methods to expose.</param>
|
||||
public void AddService(object service)
|
||||
{
|
||||
lock (this.sync)
|
||||
{
|
||||
this.targets.Add(service);
|
||||
foreach (var rpc in this.activeRpcs)
|
||||
{
|
||||
rpc.AddLocalRpcTarget(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new standalone JSON-RPC method.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the method to add.</param>
|
||||
/// <param name="handler">The handler to add.</param>
|
||||
public void AddMethod(string name, Delegate handler)
|
||||
{
|
||||
lock (this.sync)
|
||||
{
|
||||
this.methods.Add((name, handler));
|
||||
foreach (var rpc in this.activeRpcs)
|
||||
{
|
||||
rpc.AddLocalRpcMethod(name, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a JsonRpc instance <paramref name="rpc"/> to the registry so it receives all existing service targets.
|
||||
/// </summary>
|
||||
/// <param name="rpc">The JsonRpc instance to attach and populate with current targets.</param>
|
||||
internal void Attach(JsonRpc rpc)
|
||||
{
|
||||
lock (this.sync)
|
||||
{
|
||||
this.activeRpcs.Add(rpc);
|
||||
foreach (var t in this.targets)
|
||||
{
|
||||
rpc.AddLocalRpcTarget(t);
|
||||
}
|
||||
|
||||
foreach (var m in this.methods)
|
||||
{
|
||||
rpc.AddLocalRpcMethod(m.Name, m.Handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches a JsonRpc instance <paramref name="rpc"/> from the registry (e.g. when a client disconnects).
|
||||
/// </summary>
|
||||
/// <param name="rpc">The JsonRpc instance being detached.</param>
|
||||
internal void Detach(JsonRpc rpc)
|
||||
{
|
||||
lock (this.sync)
|
||||
{
|
||||
this.activeRpcs.Remove(rpc);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Dalamud/Plugin/Services/IPluginLinkHandler.cs
Normal file
23
Dalamud/Plugin/Services/IPluginLinkHandler.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Dalamud.Networking.Pipes;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the
|
||||
/// <code>dalamud://plugin/{PLUGIN_INTERNAL_NAME}/...</code> namespace.
|
||||
/// </summary>
|
||||
[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")]
|
||||
public interface IPluginLinkHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate containing the received URI.
|
||||
/// </summary>
|
||||
delegate void PluginUriReceived(DalamudUri uri);
|
||||
|
||||
/// <summary>
|
||||
/// The event fired when a URI targeting this plugin is received.
|
||||
/// </summary>
|
||||
event PluginUriReceived OnUriReceived;
|
||||
}
|
||||
328
Dalamud/Plugin/Services/IUnlockState.cs
Normal file
328
Dalamud/Plugin/Services/IUnlockState.cs
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
#pragma warning disable SA1400 // Access modifier should be declared: Interface members are public by default
|
||||
|
||||
/// <summary>
|
||||
/// Interface for determining unlock state of various content in the game.
|
||||
/// </summary>
|
||||
[Experimental("UnlockState")]
|
||||
public interface IUnlockState
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate type used for the <see cref="Unlock"/> event.
|
||||
/// </summary>
|
||||
/// <param name="rowRef">A RowRef of the unlocked thing.</param>
|
||||
delegate void UnlockDelegate(RowRef rowRef);
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when something was unlocked.
|
||||
/// </summary>
|
||||
event UnlockDelegate? Unlock;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Action is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Action row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsActionUnlocked(Lumina.Excel.Sheets.Action row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified AetherCurrentCompFlgSet is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The AetherCurrentCompFlgSet row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified AetherCurrent is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The AetherCurrent row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsAetherCurrentUnlocked(AetherCurrent row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified AozAction (Blue Mage Action) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The AozAction row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsAozActionUnlocked(AozAction row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified BannerBg (Portrait Backgrounds) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The BannerBg row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsBannerBgUnlocked(BannerBg row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified BannerCondition is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The BannerCondition row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsBannerConditionUnlocked(BannerCondition row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified BannerDecoration (Portrait Accents) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The BannerDecoration row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsBannerDecorationUnlocked(BannerDecoration row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified BannerFacial (Portrait Expressions) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The BannerFacial row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsBannerFacialUnlocked(BannerFacial row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified BannerFrame (Portrait Frames) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The BannerFrame row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsBannerFrameUnlocked(BannerFrame row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified BannerTimeline (Portrait Poses) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The BannerTimeline row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsBannerTimelineUnlocked(BannerTimeline row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified BuddyAction (Action of the players Chocobo Companion) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The BuddyAction row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsBuddyActionUnlocked(BuddyAction row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified BuddyEquip (Equipment of the players Chocobo Companion) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The BuddyEquip row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsBuddyEquipUnlocked(BuddyEquip row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified CharaMakeCustomize (Hairstyles and Face Paint patterns) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The CharaMakeCustomize row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified ChocoboTaxiStand (Chocobokeeps of the Chocobo Porter service) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The ChocoboTaxiStand row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Companion (Minions) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Companion row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsCompanionUnlocked(Companion row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified CraftAction is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The CraftAction row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsCraftActionUnlocked(CraftAction row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified CSBonusContentType is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The CSBonusContentType row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsCSBonusContentTypeUnlocked(CSBonusContentType row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Emote is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Emote row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsEmoteUnlocked(Emote row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified EmjVoiceNpc (Doman Mahjong Characters) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The EmjVoiceNpc row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified EmjCostume (Doman Mahjong Character Costume) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The EmjCostume row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsEmjCostumeUnlocked(EmjCostume row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified GeneralAction is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The GeneralAction row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsGeneralActionUnlocked(GeneralAction row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Glasses is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Glasses row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsGlassesUnlocked(Glasses row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified HowTo is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The HowTo row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsHowToUnlocked(HowTo row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified InstanceContent is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The InstanceContent row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsInstanceContentUnlocked(InstanceContent row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Item is considered unlockable.
|
||||
/// </summary>
|
||||
/// <param name="row">The Item row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlockable; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsItemUnlockable(Item row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Item is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Item row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsItemUnlocked(Item row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified McGuffin is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The McGuffin row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsMcGuffinUnlocked(McGuffin row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified MJILandmark (Island Sanctuary landmark) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The MJILandmark row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsMJILandmarkUnlocked(MJILandmark row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified MKDLore (Occult Record) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The MKDLore row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsMKDLoreUnlocked(MKDLore row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Mount is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Mount row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsMountUnlocked(Mount row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified NotebookDivision (Categories in Crafting/Gathering Log) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The NotebookDivision row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsNotebookDivisionUnlocked(NotebookDivision row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Orchestrion roll is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Orchestrion row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsOrchestrionUnlocked(Orchestrion row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Ornament (Fashion Accessories) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Ornament row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsOrnamentUnlocked(Ornament row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Perform (Performance Instruments) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Perform row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsPerformUnlocked(Perform row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified PublicContent is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The PublicContent row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsPublicContentUnlocked(PublicContent row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Recipe is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Recipe row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsRecipeUnlocked(Recipe row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the underlying RowRef type is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="rowRef">The RowRef to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsRowRefUnlocked(RowRef rowRef);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the underlying RowRef type is unlocked.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the Excel row.</typeparam>
|
||||
/// <param name="rowRef">The RowRef to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsRowRefUnlocked<T>(RowRef<T> rowRef) where T : struct, IExcelRow<T>;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified SecretRecipeBook (Master Recipe Books) is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The SecretRecipeBook row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsSecretRecipeBookUnlocked(SecretRecipeBook row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified Trait is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The Trait row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsTraitUnlocked(Trait row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified TripleTriadCard is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="row">The TripleTriadCard row to check.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsTripleTriadCardUnlocked(TripleTriadCard row);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified unlock link is unlocked or quest is completed.
|
||||
/// </summary>
|
||||
/// <param name="unlockLink">The unlock link id or quest id (quest ids in this case are over 65536).</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsUnlockLinkUnlocked(uint unlockLink);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified unlock link is unlocked.
|
||||
/// </summary>
|
||||
/// <param name="unlockLink">The unlock link id.</param>
|
||||
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
|
||||
bool IsUnlockLinkUnlocked(ushort unlockLink);
|
||||
}
|
||||
|
|
@ -3,11 +3,13 @@
|
|||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Analyzers -->
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.2" />
|
||||
|
||||
<!-- Misc Libraries -->
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.4.1" />
|
||||
<PackageVersion Include="CheapLoc" Version="1.1.8" />
|
||||
|
|
@ -22,26 +24,35 @@
|
|||
<PackageVersion Include="System.Resources.Extensions" Version="10.0.0" />
|
||||
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.39" />
|
||||
<PackageVersion Include="sqlite-net-pcl" Version="1.8.116" />
|
||||
|
||||
<!-- DirectX / Win32 -->
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
|
||||
<PackageVersion Include="SharpDX.Direct3D11" Version="4.2.0" />
|
||||
<PackageVersion Include="SharpDX.Mathematics" Version="4.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
|
||||
|
||||
<!-- Logging -->
|
||||
<PackageVersion Include="Serilog" Version="4.0.2" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
|
||||
<!-- Injector Utilities -->
|
||||
<PackageVersion Include="Iced" Version="1.17.0" />
|
||||
<PackageVersion Include="PeNet" Version="2.6.4" />
|
||||
|
||||
<!-- HexaGen -->
|
||||
<PackageVersion Include="HexaGen.Runtime" Version="1.1.20" />
|
||||
|
||||
<!-- Reloaded -->
|
||||
<PackageVersion Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp7" />
|
||||
<PackageVersion Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp5" />
|
||||
<PackageVersion Include="Reloaded.Memory" Version="7.0.0" />
|
||||
<PackageVersion Include="Reloaded.Memory.Buffers" Version="2.0.0" />
|
||||
|
||||
<!-- Named Pipes / RPC -->
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.22.23" />
|
||||
|
||||
<!-- Unit Testing -->
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
<PackageVersion Include="xunit" Version="2.4.1" />
|
||||
|
|
@ -54,4 +65,4 @@
|
|||
<PackageVersion Include="xunit.runner.console" Version="2.4.1" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue