mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-31 21:03:43 +01:00
Merge remote-tracking branch 'origin/master' into new_im_hooks-rollup
This commit is contained in:
commit
a292935f9e
43 changed files with 1393 additions and 330 deletions
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
|
|
@ -16,6 +16,9 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup MSBuild
|
- name: Setup MSBuild
|
||||||
uses: microsoft/setup-msbuild@v1.0.2
|
uses: microsoft/setup-msbuild@v1.0.2
|
||||||
|
- uses: actions/setup-dotnet@v3
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.0.100'
|
||||||
- name: Define VERSION
|
- name: Define VERSION
|
||||||
run: |
|
run: |
|
||||||
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
|
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
|
||||||
|
|
|
||||||
3
.github/workflows/rollup.yml
vendored
3
.github/workflows/rollup.yml
vendored
|
|
@ -11,8 +11,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
branches:
|
branches:
|
||||||
- net8
|
- new_im_hooks
|
||||||
#- new_im_hooks # Unmergeable
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
/// Initializes a new instance of the <see cref="GameVersion"/> class.
|
/// Initializes a new instance of the <see cref="GameVersion"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="version">Version string to parse.</param>
|
/// <param name="version">Version string to parse.</param>
|
||||||
[JsonConstructor]
|
|
||||||
public GameVersion(string version)
|
public GameVersion(string version)
|
||||||
{
|
{
|
||||||
var ver = Parse(version);
|
var ver = Parse(version);
|
||||||
|
|
@ -42,20 +41,9 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
/// <param name="day">The day.</param>
|
/// <param name="day">The day.</param>
|
||||||
/// <param name="major">The major version.</param>
|
/// <param name="major">The major version.</param>
|
||||||
/// <param name="minor">The minor version.</param>
|
/// <param name="minor">The minor version.</param>
|
||||||
public GameVersion(int year, int month, int day, int major, int minor)
|
[JsonConstructor]
|
||||||
|
public GameVersion(int year, int month, int day, int major, int minor) : this(year, month, day, major)
|
||||||
{
|
{
|
||||||
if ((this.Year = year) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(year));
|
|
||||||
|
|
||||||
if ((this.Month = month) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(month));
|
|
||||||
|
|
||||||
if ((this.Day = day) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(day));
|
|
||||||
|
|
||||||
if ((this.Major = major) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(major));
|
|
||||||
|
|
||||||
if ((this.Minor = minor) < 0)
|
if ((this.Minor = minor) < 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(minor));
|
throw new ArgumentOutOfRangeException(nameof(minor));
|
||||||
}
|
}
|
||||||
|
|
@ -67,17 +55,8 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
/// <param name="month">The month.</param>
|
/// <param name="month">The month.</param>
|
||||||
/// <param name="day">The day.</param>
|
/// <param name="day">The day.</param>
|
||||||
/// <param name="major">The major version.</param>
|
/// <param name="major">The major version.</param>
|
||||||
public GameVersion(int year, int month, int day, int major)
|
public GameVersion(int year, int month, int day, int major) : this(year, month, day)
|
||||||
{
|
{
|
||||||
if ((this.Year = year) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(year));
|
|
||||||
|
|
||||||
if ((this.Month = month) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(month));
|
|
||||||
|
|
||||||
if ((this.Day = day) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(day));
|
|
||||||
|
|
||||||
if ((this.Major = major) < 0)
|
if ((this.Major = major) < 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(major));
|
throw new ArgumentOutOfRangeException(nameof(major));
|
||||||
}
|
}
|
||||||
|
|
@ -88,14 +67,8 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
/// <param name="year">The year.</param>
|
/// <param name="year">The year.</param>
|
||||||
/// <param name="month">The month.</param>
|
/// <param name="month">The month.</param>
|
||||||
/// <param name="day">The day.</param>
|
/// <param name="day">The day.</param>
|
||||||
public GameVersion(int year, int month, int day)
|
public GameVersion(int year, int month, int day) : this(year, month)
|
||||||
{
|
{
|
||||||
if ((this.Year = year) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(year));
|
|
||||||
|
|
||||||
if ((this.Month = month) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(month));
|
|
||||||
|
|
||||||
if ((this.Day = day) < 0)
|
if ((this.Day = day) < 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(day));
|
throw new ArgumentOutOfRangeException(nameof(day));
|
||||||
}
|
}
|
||||||
|
|
@ -105,11 +78,8 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="year">The year.</param>
|
/// <param name="year">The year.</param>
|
||||||
/// <param name="month">The month.</param>
|
/// <param name="month">The month.</param>
|
||||||
public GameVersion(int year, int month)
|
public GameVersion(int year, int month) : this(year)
|
||||||
{
|
{
|
||||||
if ((this.Year = year) < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(year));
|
|
||||||
|
|
||||||
if ((this.Month = month) < 0)
|
if ((this.Month = month) < 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(month));
|
throw new ArgumentOutOfRangeException(nameof(month));
|
||||||
}
|
}
|
||||||
|
|
@ -139,26 +109,31 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the year component.
|
/// Gets the year component.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonRequired]
|
||||||
public int Year { get; } = -1;
|
public int Year { get; } = -1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the month component.
|
/// Gets the month component.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonRequired]
|
||||||
public int Month { get; } = -1;
|
public int Month { get; } = -1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the day component.
|
/// Gets the day component.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonRequired]
|
||||||
public int Day { get; } = -1;
|
public int Day { get; } = -1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the major version component.
|
/// Gets the major version component.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonRequired]
|
||||||
public int Major { get; } = -1;
|
public int Major { get; } = -1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the minor version component.
|
/// Gets the minor version component.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonRequired]
|
||||||
public int Minor { get; } = -1;
|
public int Minor { get; } = -1;
|
||||||
|
|
||||||
public static implicit operator GameVersion(string ver)
|
public static implicit operator GameVersion(string ver)
|
||||||
|
|
@ -183,17 +158,13 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
|
|
||||||
public static bool operator <(GameVersion v1, GameVersion v2)
|
public static bool operator <(GameVersion v1, GameVersion v2)
|
||||||
{
|
{
|
||||||
if (v1 is null)
|
ArgumentNullException.ThrowIfNull(v1);
|
||||||
throw new ArgumentNullException(nameof(v1));
|
|
||||||
|
|
||||||
return v1.CompareTo(v2) < 0;
|
return v1.CompareTo(v2) < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool operator <=(GameVersion v1, GameVersion v2)
|
public static bool operator <=(GameVersion v1, GameVersion v2)
|
||||||
{
|
{
|
||||||
if (v1 is null)
|
ArgumentNullException.ThrowIfNull(v1);
|
||||||
throw new ArgumentNullException(nameof(v1));
|
|
||||||
|
|
||||||
return v1.CompareTo(v2) <= 0;
|
return v1.CompareTo(v2) <= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,8 +180,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
|
|
||||||
public static GameVersion operator +(GameVersion v1, TimeSpan v2)
|
public static GameVersion operator +(GameVersion v1, TimeSpan v2)
|
||||||
{
|
{
|
||||||
if (v1 == null)
|
ArgumentNullException.ThrowIfNull(v1);
|
||||||
throw new ArgumentNullException(nameof(v1));
|
|
||||||
|
|
||||||
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
|
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
|
||||||
return v1;
|
return v1;
|
||||||
|
|
@ -222,8 +192,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
|
|
||||||
public static GameVersion operator -(GameVersion v1, TimeSpan v2)
|
public static GameVersion operator -(GameVersion v1, TimeSpan v2)
|
||||||
{
|
{
|
||||||
if (v1 == null)
|
ArgumentNullException.ThrowIfNull(v1);
|
||||||
throw new ArgumentNullException(nameof(v1));
|
|
||||||
|
|
||||||
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
|
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
|
||||||
return v1;
|
return v1;
|
||||||
|
|
@ -240,18 +209,18 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
/// <returns>GameVersion object.</returns>
|
/// <returns>GameVersion object.</returns>
|
||||||
public static GameVersion Parse(string input)
|
public static GameVersion Parse(string input)
|
||||||
{
|
{
|
||||||
if (input == null)
|
ArgumentNullException.ThrowIfNull(input);
|
||||||
throw new ArgumentNullException(nameof(input));
|
|
||||||
|
|
||||||
if (input.ToLower(CultureInfo.InvariantCulture) == "any")
|
if (input.ToLower(CultureInfo.InvariantCulture) == "any")
|
||||||
return new GameVersion();
|
return Any;
|
||||||
|
|
||||||
var parts = input.Split('.');
|
var parts = input.Split('.');
|
||||||
var tplParts = parts.Select(p =>
|
var tplParts = parts.Select(
|
||||||
{
|
p =>
|
||||||
var result = int.TryParse(p, out var value);
|
{
|
||||||
return (result, value);
|
var result = int.TryParse(p, out var value);
|
||||||
}).ToArray();
|
return (result, value);
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
if (tplParts.Any(t => !t.result))
|
if (tplParts.Any(t => !t.result))
|
||||||
throw new FormatException("Bad formatting");
|
throw new FormatException("Bad formatting");
|
||||||
|
|
@ -259,18 +228,15 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
var intParts = tplParts.Select(t => t.value).ToArray();
|
var intParts = tplParts.Select(t => t.value).ToArray();
|
||||||
var len = intParts.Length;
|
var len = intParts.Length;
|
||||||
|
|
||||||
if (len == 1)
|
return len switch
|
||||||
return new GameVersion(intParts[0]);
|
{
|
||||||
else if (len == 2)
|
1 => new GameVersion(intParts[0]),
|
||||||
return new GameVersion(intParts[0], intParts[1]);
|
2 => new GameVersion(intParts[0], intParts[1]),
|
||||||
else if (len == 3)
|
3 => new GameVersion(intParts[0], intParts[1], intParts[2]),
|
||||||
return new GameVersion(intParts[0], intParts[1], intParts[2]);
|
4 => new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3]),
|
||||||
else if (len == 4)
|
5 => new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3], intParts[4]),
|
||||||
return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3]);
|
_ => throw new ArgumentException("Too many parts"),
|
||||||
else if (len == 5)
|
};
|
||||||
return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3], intParts[4]);
|
|
||||||
else
|
|
||||||
throw new ArgumentException("Too many parts");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -299,17 +265,12 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public int CompareTo(object? obj)
|
public int CompareTo(object? obj)
|
||||||
{
|
{
|
||||||
if (obj == null)
|
return obj switch
|
||||||
return 1;
|
|
||||||
|
|
||||||
if (obj is GameVersion value)
|
|
||||||
{
|
{
|
||||||
return this.CompareTo(value);
|
null => 1,
|
||||||
}
|
GameVersion value => this.CompareTo(value),
|
||||||
else
|
_ => throw new ArgumentException("Argument must be a GameVersion", nameof(obj)),
|
||||||
{
|
};
|
||||||
throw new ArgumentException("Argument must be a GameVersion");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
@ -342,16 +303,14 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
if (this.Minor != value.Minor)
|
if (this.Minor != value.Minor)
|
||||||
return this.Minor > value.Minor ? 1 : -1;
|
return this.Minor > value.Minor ? 1 : -1;
|
||||||
|
|
||||||
|
// This should never happen
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override bool Equals(object? obj)
|
public override bool Equals(object? obj)
|
||||||
{
|
{
|
||||||
if (obj is not GameVersion value)
|
return obj is GameVersion value && this.Equals(value);
|
||||||
return false;
|
|
||||||
|
|
||||||
return this.Equals(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
@ -373,16 +332,8 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
var accumulator = 0;
|
// https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-8.0#notes-to-inheritors
|
||||||
|
return HashCode.Combine(this.Year, this.Month, this.Day, this.Major, this.Minor);
|
||||||
// This might be horribly wrong, but it isn't used heavily.
|
|
||||||
accumulator |= this.Year.GetHashCode();
|
|
||||||
accumulator |= this.Month.GetHashCode();
|
|
||||||
accumulator |= this.Day.GetHashCode();
|
|
||||||
accumulator |= this.Major.GetHashCode();
|
|
||||||
accumulator |= this.Minor.GetHashCode();
|
|
||||||
|
|
||||||
return accumulator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
@ -396,11 +347,11 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersi
|
||||||
return "any";
|
return "any";
|
||||||
|
|
||||||
return new StringBuilder()
|
return new StringBuilder()
|
||||||
.Append(string.Format("{0:D4}.", this.Year == -1 ? 0 : this.Year))
|
.Append($"{(this.Year == -1 ? 0 : this.Year):D4}.")
|
||||||
.Append(string.Format("{0:D2}.", this.Month == -1 ? 0 : this.Month))
|
.Append($"{(this.Month == -1 ? 0 : this.Month):D2}.")
|
||||||
.Append(string.Format("{0:D2}.", this.Day == -1 ? 0 : this.Day))
|
.Append($"{(this.Day == -1 ? 0 : this.Day):D2}.")
|
||||||
.Append(string.Format("{0:D4}.", this.Major == -1 ? 0 : this.Major))
|
.Append($"{(this.Major == -1 ? 0 : this.Major):D4}.")
|
||||||
.Append(string.Format("{0:D4}", this.Minor == -1 ? 0 : this.Minor))
|
.Append($"{(this.Minor == -1 ? 0 : this.Minor):D4}")
|
||||||
.ToString();
|
.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,16 @@ public sealed class GameVersionConverter : JsonConverter
|
||||||
/// <param name="serializer">The calling serializer.</param>
|
/// <param name="serializer">The calling serializer.</param>
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
if (value == null)
|
switch (value)
|
||||||
{
|
{
|
||||||
writer.WriteNull();
|
case null:
|
||||||
}
|
writer.WriteNull();
|
||||||
else if (value is GameVersion)
|
break;
|
||||||
{
|
case GameVersion:
|
||||||
writer.WriteValue(value.ToString());
|
writer.WriteValue(value.ToString());
|
||||||
}
|
break;
|
||||||
else
|
default:
|
||||||
{
|
throw new JsonSerializationException("Expected GameVersion object value");
|
||||||
throw new JsonSerializationException("Expected GameVersion object value");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,24 +42,20 @@ public sealed class GameVersionConverter : JsonConverter
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (reader.TokenType == JsonToken.String)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonToken.String)
|
try
|
||||||
{
|
{
|
||||||
try
|
return new GameVersion((string)reader.Value!);
|
||||||
{
|
|
||||||
return new GameVersion((string)reader.Value!);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new JsonSerializationException($"Error parsing GameVersion string: {reader.Value}", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new JsonSerializationException($"Unexpected token or value when parsing GameVersion. Token: {reader.TokenType}, Value: {reader.Value}");
|
throw new JsonSerializationException($"Error parsing GameVersion string: {reader.Value}", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new JsonSerializationException($"Unexpected token or value when parsing GameVersion. Token: {reader.TokenType}, Value: {reader.Value}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>Dalamud.CorePlugin</AssemblyName>
|
<AssemblyName>Dalamud.CorePlugin</AssemblyName>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<Platforms>x64</Platforms>
|
<Platforms>x64</Platforms>
|
||||||
<LangVersion>10.0</LangVersion>
|
<LangVersion>10.0</LangVersion>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup Label="Target">
|
<PropertyGroup Label="Target">
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
<Platforms>x64;AnyCPU</Platforms>
|
<Platforms>x64;AnyCPU</Platforms>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup Label="Target">
|
<PropertyGroup Label="Target">
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
<Platforms>x64;AnyCPU</Platforms>
|
<Platforms>x64;AnyCPU</Platforms>
|
||||||
<LangVersion>9.0</LangVersion>
|
<LangVersion>11.0</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Label="Feature">
|
<PropertyGroup Label="Feature">
|
||||||
|
|
|
||||||
138
Dalamud.Test/Game/GameVersionConverterTests.cs
Normal file
138
Dalamud.Test/Game/GameVersionConverterTests.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
using Dalamud.Common.Game;
|
||||||
|
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Dalamud.Test.Game;
|
||||||
|
|
||||||
|
public class GameVersionConverterTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ReadJson_ConvertsFromString()
|
||||||
|
{
|
||||||
|
var serialized = """
|
||||||
|
{
|
||||||
|
"Version": "2020.06.15.0000.0000"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
var deserialized = JsonConvert.DeserializeObject<TestSerializationClass>(serialized);
|
||||||
|
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
Assert.Equal(GameVersion.Parse("2020.06.15.0000.0000"), deserialized.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadJson_ConvertsFromNull()
|
||||||
|
{
|
||||||
|
var serialized = """
|
||||||
|
{
|
||||||
|
"Version": null
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
var deserialized = JsonConvert.DeserializeObject<TestSerializationClass>(serialized);
|
||||||
|
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
Assert.Null(deserialized.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadJson_WhenInvalidType_Throws()
|
||||||
|
{
|
||||||
|
var serialized = """
|
||||||
|
{
|
||||||
|
"Version": 2
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Assert.Throws<JsonSerializationException>(
|
||||||
|
() => JsonConvert.DeserializeObject<TestSerializationClass>(serialized));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadJson_WhenInvalidVersion_Throws()
|
||||||
|
{
|
||||||
|
var serialized = """
|
||||||
|
{
|
||||||
|
"Version": "junk"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Assert.Throws<JsonSerializationException>(
|
||||||
|
() => JsonConvert.DeserializeObject<TestSerializationClass>(serialized));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteJson_ConvertsToString()
|
||||||
|
{
|
||||||
|
var deserialized = new TestSerializationClass
|
||||||
|
{
|
||||||
|
Version = GameVersion.Parse("2020.06.15.0000.0000"),
|
||||||
|
};
|
||||||
|
var serialized = JsonConvert.SerializeObject(deserialized);
|
||||||
|
|
||||||
|
Assert.Equal("""{"Version":"2020.06.15.0000.0000"}""", RemoveWhitespace(serialized));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteJson_ConvertsToNull()
|
||||||
|
{
|
||||||
|
var deserialized = new TestSerializationClass
|
||||||
|
{
|
||||||
|
Version = null,
|
||||||
|
};
|
||||||
|
var serialized = JsonConvert.SerializeObject(deserialized);
|
||||||
|
|
||||||
|
Assert.Equal("""{"Version":null}""", RemoveWhitespace(serialized));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteJson_WhenInvalidVersion_Throws()
|
||||||
|
{
|
||||||
|
var deserialized = new TestWrongTypeSerializationClass
|
||||||
|
{
|
||||||
|
Version = 42,
|
||||||
|
};
|
||||||
|
Assert.Throws<JsonSerializationException>(() => JsonConvert.SerializeObject(deserialized));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanConvert_WhenGameVersion_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var converter = new GameVersionConverter();
|
||||||
|
Assert.True(converter.CanConvert(typeof(GameVersion)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanConvert_WhenNotGameVersion_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var converter = new GameVersionConverter();
|
||||||
|
Assert.False(converter.CanConvert(typeof(int)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanConvert_WhenNull_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var converter = new GameVersionConverter();
|
||||||
|
Assert.False(converter.CanConvert(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveWhitespace(string input)
|
||||||
|
{
|
||||||
|
return input.Replace(" ", "").Replace("\r", "").Replace("\n", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestSerializationClass
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(GameVersionConverter))]
|
||||||
|
[CanBeNull]
|
||||||
|
public GameVersion Version { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestWrongTypeSerializationClass
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(GameVersionConverter))]
|
||||||
|
public int Version { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,71 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
using Dalamud.Common.Game;
|
using Dalamud.Common.Game;
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Dalamud.Test.Game
|
namespace Dalamud.Test.Game
|
||||||
{
|
{
|
||||||
public class GameVersionTests
|
public class GameVersionTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void VersionComparisons()
|
||||||
|
{
|
||||||
|
var v1 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||||
|
var v2 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||||
|
Assert.True(v1 == v2);
|
||||||
|
Assert.False(v1 != v2);
|
||||||
|
Assert.False(v1 < v2);
|
||||||
|
Assert.True(v1 <= v2);
|
||||||
|
Assert.False(v1 > v2);
|
||||||
|
Assert.True(v1 >= v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionAddition()
|
||||||
|
{
|
||||||
|
var v1 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||||
|
var v2 = GameVersion.Parse("2021.01.05.0000.0000");
|
||||||
|
Assert.Equal(v2, v1 + TimeSpan.FromDays(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionAdditionAny()
|
||||||
|
{
|
||||||
|
Assert.Equal(GameVersion.Any, GameVersion.Any + TimeSpan.FromDays(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionSubtraction()
|
||||||
|
{
|
||||||
|
var v1 = GameVersion.Parse("2021.01.05.0000.0000");
|
||||||
|
var v2 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||||
|
Assert.Equal(v2, v1 - TimeSpan.FromDays(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionSubtractionAny()
|
||||||
|
{
|
||||||
|
Assert.Equal(GameVersion.Any, GameVersion.Any - TimeSpan.FromDays(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionClone()
|
||||||
|
{
|
||||||
|
var v1 = GameVersion.Parse("2021.01.01.0000.0000");
|
||||||
|
var v2 = v1.Clone();
|
||||||
|
Assert.NotSame(v1, v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionCast()
|
||||||
|
{
|
||||||
|
var v = GameVersion.Parse("2021.01.01.0000.0000");
|
||||||
|
Assert.Equal("2021.01.01.0000.0000", v);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("any", "any")]
|
[InlineData("any", "any")]
|
||||||
[InlineData("2021.01.01.0000.0000", "2021.01.01.0000.0000")]
|
[InlineData("2021.01.01.0000.0000", "2021.01.01.0000.0000")]
|
||||||
|
|
@ -14,6 +75,18 @@ namespace Dalamud.Test.Game
|
||||||
var v2 = GameVersion.Parse(ver2);
|
var v2 = GameVersion.Parse(ver2);
|
||||||
|
|
||||||
Assert.Equal(v1, v2);
|
Assert.Equal(v1, v2);
|
||||||
|
Assert.Equal(0, v1.CompareTo(v2));
|
||||||
|
Assert.Equal(v1.GetHashCode(), v2.GetHashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionNullEquality()
|
||||||
|
{
|
||||||
|
// Tests `Equals(GameVersion? value)`
|
||||||
|
Assert.False(GameVersion.Parse("2021.01.01.0000.0000").Equals(null));
|
||||||
|
|
||||||
|
// Tests `Equals(object? value)`
|
||||||
|
Assert.False(GameVersion.Parse("2021.01.01.0000.0000").Equals((object)null));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -31,6 +104,67 @@ namespace Dalamud.Test.Game
|
||||||
Assert.True(v1.CompareTo(v2) < 0);
|
Assert.True(v1.CompareTo(v2) < 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("any", "2020.06.15.0000.0000")]
|
||||||
|
public void VersionComparisonInverse(string ver1, string ver2)
|
||||||
|
{
|
||||||
|
var v1 = GameVersion.Parse(ver1);
|
||||||
|
var v2 = GameVersion.Parse(ver2);
|
||||||
|
|
||||||
|
Assert.True(v1.CompareTo(v2) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionComparisonNull()
|
||||||
|
{
|
||||||
|
var v = GameVersion.Parse("2020.06.15.0000.0000");
|
||||||
|
|
||||||
|
// Tests `CompareTo(GameVersion? value)`
|
||||||
|
Assert.True(v.CompareTo(null) > 0);
|
||||||
|
|
||||||
|
// Tests `CompareTo(object? value)`
|
||||||
|
Assert.True(v.CompareTo((object)null) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionComparisonBoxed()
|
||||||
|
{
|
||||||
|
var v1 = GameVersion.Parse("2020.06.15.0000.0000");
|
||||||
|
var v2 = GameVersion.Parse("2020.06.15.0000.0000");
|
||||||
|
Assert.Equal(0, v1.CompareTo((object)v2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionComparisonBoxedInvalid()
|
||||||
|
{
|
||||||
|
var v = GameVersion.Parse("2020.06.15.0000.0000");
|
||||||
|
Assert.Throws<ArgumentException>(() => v.CompareTo(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("2020.06.15.0000.0000")]
|
||||||
|
[InlineData("2021.01.01.0000")]
|
||||||
|
[InlineData("2021.01.01")]
|
||||||
|
[InlineData("2021.01")]
|
||||||
|
[InlineData("2021")]
|
||||||
|
public void VersionParse(string ver)
|
||||||
|
{
|
||||||
|
var v = GameVersion.Parse(ver);
|
||||||
|
Assert.NotNull(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("2020.06.15.0000.0000")]
|
||||||
|
[InlineData("2021.01.01.0000")]
|
||||||
|
[InlineData("2021.01.01")]
|
||||||
|
[InlineData("2021.01")]
|
||||||
|
[InlineData("2021")]
|
||||||
|
public void VersionTryParse(string ver)
|
||||||
|
{
|
||||||
|
Assert.True(GameVersion.TryParse(ver, out var v));
|
||||||
|
Assert.NotNull(v);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("2020.06.15.0000.0000")]
|
[InlineData("2020.06.15.0000.0000")]
|
||||||
[InlineData("2021.01.01.0000")]
|
[InlineData("2021.01.01.0000")]
|
||||||
|
|
@ -39,9 +173,8 @@ namespace Dalamud.Test.Game
|
||||||
[InlineData("2021")]
|
[InlineData("2021")]
|
||||||
public void VersionConstructor(string ver)
|
public void VersionConstructor(string ver)
|
||||||
{
|
{
|
||||||
var v = GameVersion.Parse(ver);
|
var v = new GameVersion(ver);
|
||||||
|
Assert.NotNull(v);
|
||||||
Assert.True(v != null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -54,5 +187,89 @@ namespace Dalamud.Test.Game
|
||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
Assert.Null(v);
|
Assert.Null(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("any", "any")]
|
||||||
|
[InlineData("2020.06.15.0000.0000", "2020.06.15.0000.0000")]
|
||||||
|
[InlineData("2021.01.01.0000", "2021.01.01.0000.0000")]
|
||||||
|
[InlineData("2021.01.01", "2021.01.01.0000.0000")]
|
||||||
|
[InlineData("2021.01", "2021.01.00.0000.0000")]
|
||||||
|
[InlineData("2021", "2021.00.00.0000.0000")]
|
||||||
|
public void VersionToString(string ver1, string ver2)
|
||||||
|
{
|
||||||
|
var v1 = GameVersion.Parse(ver1);
|
||||||
|
Assert.Equal(ver2, v1.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionIsSerializationSafe()
|
||||||
|
{
|
||||||
|
var v = GameVersion.Parse("2020.06.15.0000.0000");
|
||||||
|
var serialized = JsonConvert.SerializeObject(v);
|
||||||
|
var deserialized = JsonConvert.DeserializeObject<GameVersion>(serialized);
|
||||||
|
Assert.Equal(v, deserialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionInvalidDeserialization()
|
||||||
|
{
|
||||||
|
var serialized = """
|
||||||
|
{
|
||||||
|
"Year": -1,
|
||||||
|
"Month": -1,
|
||||||
|
"Day": -1,
|
||||||
|
"Major": -1,
|
||||||
|
"Minor": -1,
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => JsonConvert.DeserializeObject<GameVersion>(serialized));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionInvalidTypeDeserialization()
|
||||||
|
{
|
||||||
|
var serialized = """
|
||||||
|
{
|
||||||
|
"Value": "Hello"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Assert.Throws<JsonSerializationException>(() => JsonConvert.DeserializeObject<GameVersion>(serialized));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionConstructorNegativeYear()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(-2024));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionConstructorNegativeMonth()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(2024, -3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionConstructorNegativeDay()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(2024, 3, -13));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionConstructorNegativeMajor()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(2024, 3, 13, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionConstructorNegativeMinor()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => new GameVersion(2024, 3, 13, 0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VersionParseNull()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => GameVersion.Parse(null!));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
386
Dalamud.Test/Storage/ReliableFileStorageTests.cs
Normal file
386
Dalamud.Test/Storage/ReliableFileStorageTests.cs
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Dalamud.Storage;
|
||||||
|
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Dalamud.Test.Storage;
|
||||||
|
|
||||||
|
public class ReliableFileStorageTests
|
||||||
|
{
|
||||||
|
private const string DbFileName = "dalamudVfs.db";
|
||||||
|
private const string TestFileName = "file.txt";
|
||||||
|
private const string TestFileContent1 = "hello from señor dalamundo";
|
||||||
|
private const string TestFileContent2 = "rewritten";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IsConcurrencySafe()
|
||||||
|
{
|
||||||
|
var dbDir = CreateTempDir();
|
||||||
|
using var rfs = new ReliableFileStorage(dbDir);
|
||||||
|
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
|
||||||
|
// Do reads/writes/deletes on the same file on many threads at once and
|
||||||
|
// see if anything throws
|
||||||
|
await Task.WhenAll(
|
||||||
|
Enumerable.Range(1, 6)
|
||||||
|
.Select(
|
||||||
|
i => Parallel.ForEachAsync(
|
||||||
|
Enumerable.Range(1, 100),
|
||||||
|
(j, _) =>
|
||||||
|
{
|
||||||
|
if (i % 2 == 0)
|
||||||
|
{
|
||||||
|
// ReSharper disable once AccessToDisposedClosure
|
||||||
|
rfs.WriteAllText(tempFile, j.ToString());
|
||||||
|
}
|
||||||
|
else if (i % 3 == 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ReSharper disable once AccessToDisposedClosure
|
||||||
|
rfs.ReadAllText(tempFile);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
// this is fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Delete(tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_Dispose_Works()
|
||||||
|
{
|
||||||
|
var dbDir = CreateTempDir();
|
||||||
|
var dbPath = Path.Combine(dbDir, DbFileName);
|
||||||
|
using var rfs = new ReliableFileStorage(dbDir);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(dbPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Exists_ThrowsIfPathIsEmpty()
|
||||||
|
{
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
Assert.Throws<ArgumentException>(() => rfs.Exists(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Exists_ThrowsIfPathIsNull()
|
||||||
|
{
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
Assert.Throws<ArgumentNullException>(() => rfs.Exists(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Exists_WhenFileMissing_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
Assert.False(rfs.Exists(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Exists_WhenFileMissing_WhenDbFailed_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateFailedRfs();
|
||||||
|
|
||||||
|
Assert.False(rfs.Exists(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Exists_WhenFileOnDisk_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
Assert.True(rfs.Exists(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Exists_WhenFileInBackup_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent1);
|
||||||
|
|
||||||
|
File.Delete(tempFile);
|
||||||
|
Assert.True(rfs.Exists(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent1);
|
||||||
|
|
||||||
|
File.Delete(tempFile);
|
||||||
|
Assert.False(rfs.Exists(tempFile, Guid.NewGuid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteAllText_ThrowsIfPathIsEmpty()
|
||||||
|
{
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
Assert.Throws<ArgumentException>(() => rfs.WriteAllText("", TestFileContent1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteAllText_ThrowsIfPathIsNull()
|
||||||
|
{
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
Assert.Throws<ArgumentNullException>(() => rfs.WriteAllText(null!, TestFileContent1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WriteAllText_WritesToDbAndDisk()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent1);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(tempFile));
|
||||||
|
Assert.Equal(TestFileContent1, rfs.ReadAllText(tempFile, forceBackup: true));
|
||||||
|
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteAllText_SeparatesContainers()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
var containerId = Guid.NewGuid();
|
||||||
|
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent1);
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent2, containerId);
|
||||||
|
File.Delete(tempFile);
|
||||||
|
|
||||||
|
Assert.Equal(TestFileContent1, rfs.ReadAllText(tempFile, forceBackup: true));
|
||||||
|
Assert.Equal(TestFileContent2, rfs.ReadAllText(tempFile, forceBackup: true, containerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WriteAllText_WhenDbFailed_WritesToDisk()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateFailedRfs();
|
||||||
|
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent1);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(tempFile));
|
||||||
|
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WriteAllText_CanUpdateExistingFile()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent1);
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent2);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(tempFile));
|
||||||
|
Assert.Equal(TestFileContent2, rfs.ReadAllText(tempFile, forceBackup: true));
|
||||||
|
Assert.Equal(TestFileContent2, await File.ReadAllTextAsync(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteAllText_SupportsNullContent()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
rfs.WriteAllText(tempFile, null);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(tempFile));
|
||||||
|
Assert.Equal("", rfs.ReadAllText(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadAllText_ThrowsIfPathIsEmpty()
|
||||||
|
{
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
Assert.Throws<ArgumentException>(() => rfs.ReadAllText(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadAllText_ThrowsIfPathIsNull()
|
||||||
|
{
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
Assert.Throws<ArgumentNullException>(() => rfs.ReadAllText(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadAllText_WhenFileOnDisk_ReturnsContent()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
Assert.Equal(TestFileContent1, rfs.ReadAllText(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadAllText_WhenFileMissingWithBackup_ReturnsContent()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent1);
|
||||||
|
File.Delete(tempFile);
|
||||||
|
|
||||||
|
Assert.Equal(TestFileContent1, rfs.ReadAllText(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
var containerId = Guid.NewGuid();
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent1);
|
||||||
|
File.Delete(tempFile);
|
||||||
|
|
||||||
|
Assert.Throws<FileNotFoundException>(() => rfs.ReadAllText(tempFile, containerId: containerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadAllText_WhenFileMissing_ThrowsIfDbFailed()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateFailedRfs();
|
||||||
|
Assert.Throws<FileNotFoundException>(() => rfs.ReadAllText(tempFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadAllText_WithReader_WhenFileOnDisk_ReadsContent()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
rfs.ReadAllText(tempFile, text => Assert.Equal(TestFileContent1, text));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadAllText_WithReader_WhenReaderThrows_ThrowsIfBackupMissing()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||||
|
|
||||||
|
var readerCalledOnce = false;
|
||||||
|
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
Assert.Throws<FileReadException>(() => rfs.ReadAllText(tempFile, Reader));
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
void Reader(string text)
|
||||||
|
{
|
||||||
|
var wasReaderCalledOnce = readerCalledOnce;
|
||||||
|
readerCalledOnce = true;
|
||||||
|
if (!wasReaderCalledOnce) throw new Exception();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
|
||||||
|
var readerCalledOnce = false;
|
||||||
|
var assertionCalled = false;
|
||||||
|
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
rfs.WriteAllText(tempFile, TestFileContent1);
|
||||||
|
File.Delete(tempFile);
|
||||||
|
|
||||||
|
rfs.ReadAllText(tempFile, Reader);
|
||||||
|
Assert.True(assertionCalled);
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
void Reader(string text)
|
||||||
|
{
|
||||||
|
var wasReaderCalledOnce = readerCalledOnce;
|
||||||
|
readerCalledOnce = true;
|
||||||
|
if (!wasReaderCalledOnce) throw new Exception();
|
||||||
|
Assert.Equal(TestFileContent1, text);
|
||||||
|
assertionCalled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadAllText_WithReader_RethrowsFileNotFoundException()
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
await File.WriteAllTextAsync(tempFile, TestFileContent1);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
Assert.Throws<FileNotFoundException>(() => rfs.ReadAllText(tempFile, _ => throw new FileNotFoundException()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup)
|
||||||
|
{
|
||||||
|
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
|
||||||
|
using var rfs = CreateRfs();
|
||||||
|
Assert.Throws<FileNotFoundException>(() => rfs.ReadAllText(tempFile, forceBackup));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReliableFileStorage CreateRfs()
|
||||||
|
{
|
||||||
|
var dbDir = CreateTempDir();
|
||||||
|
return new ReliableFileStorage(dbDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReliableFileStorage CreateFailedRfs()
|
||||||
|
{
|
||||||
|
var dbDir = CreateTempDir();
|
||||||
|
var dbPath = Path.Combine(dbDir, DbFileName);
|
||||||
|
|
||||||
|
// Create a corrupt DB deliberately, and hold its handle until
|
||||||
|
// the end of the scope
|
||||||
|
using var f = File.Open(dbPath, FileMode.CreateNew);
|
||||||
|
f.Write("broken"u8);
|
||||||
|
|
||||||
|
// Throws an SQLiteException initially, and then throws an
|
||||||
|
// IOException when attempting to delete the file because
|
||||||
|
// there's already an active handle associated with it
|
||||||
|
return new ReliableFileStorage(dbDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateTempDir()
|
||||||
|
{
|
||||||
|
string tempDir;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Generate temp directories until we get a new one (usually happens on the first try)
|
||||||
|
tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||||
|
}
|
||||||
|
while (File.Exists(tempDir));
|
||||||
|
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Interface.FontIdentifier;
|
using Dalamud.Interface.FontIdentifier;
|
||||||
|
|
@ -367,6 +369,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ShowTsm { get; set; } = true;
|
public bool ShowTsm { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to reduce motions (animations).
|
||||||
|
/// </summary>
|
||||||
|
public bool? ReduceMotions { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether or not market board data should be uploaded.
|
/// Gets or sets a value indicating whether or not market board data should be uploaded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -481,6 +488,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
||||||
|
|
||||||
deserialized ??= new DalamudConfiguration();
|
deserialized ??= new DalamudConfiguration();
|
||||||
deserialized.configPath = path;
|
deserialized.configPath = path;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
deserialized.SetDefaults();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e, "Failed to set defaults for DalamudConfiguration");
|
||||||
|
}
|
||||||
|
|
||||||
return deserialized;
|
return deserialized;
|
||||||
}
|
}
|
||||||
|
|
@ -522,6 +538,31 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetDefaults()
|
||||||
|
{
|
||||||
|
// "Reduced motion"
|
||||||
|
if (!this.ReduceMotions.HasValue)
|
||||||
|
{
|
||||||
|
// https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/animation/animation_win.cc;l=29?q=ReducedMotion&ss=chromium
|
||||||
|
var winAnimEnabled = 0;
|
||||||
|
var success = NativeFunctions.SystemParametersInfo(
|
||||||
|
(uint)NativeFunctions.AccessibilityParameter.SPI_GETCLIENTAREAANIMATION,
|
||||||
|
0,
|
||||||
|
ref winAnimEnabled,
|
||||||
|
0);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
Log.Warning("Failed to get Windows animation setting, assuming reduced motion is off (GetLastError: {GetLastError:X})", Marshal.GetLastPInvokeError());
|
||||||
|
this.ReduceMotions = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.ReduceMotions = winAnimEnabled == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Save()
|
private void Save()
|
||||||
{
|
{
|
||||||
ThreadSafety.AssertMainThread();
|
ThreadSafety.AssertMainThread();
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup Label="Target">
|
<PropertyGroup Label="Target">
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
<Platforms>x64;AnyCPU</Platforms>
|
<Platforms>x64;AnyCPU</Platforms>
|
||||||
<LangVersion>11.0</LangVersion>
|
<LangVersion>12.0</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Label="Feature">
|
<PropertyGroup Label="Feature">
|
||||||
<DalamudVersion>9.0.0.21</DalamudVersion>
|
<DalamudVersion>9.1.0.0</DalamudVersion>
|
||||||
<Description>XIV Launcher addon framework</Description>
|
<Description>XIV Launcher addon framework</Description>
|
||||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||||
<Version>$(DalamudVersion)</Version>
|
<Version>$(DalamudVersion)</Version>
|
||||||
|
|
@ -75,7 +75,6 @@
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MinSharp" Version="1.0.4" />
|
<PackageReference Include="MinSharp" Version="1.0.4" />
|
||||||
<PackageReference Include="MonoModReorg.RuntimeDetour" Version="23.1.2-prerelease.1" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||||
<PackageReference Include="Serilog" Version="2.11.0" />
|
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||||
|
|
@ -119,14 +118,6 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="ChangeAliasesOfNugetRefs" BeforeTargets="FindReferenceAssembliesForReferences;ResolveReferences">
|
|
||||||
<ItemGroup>
|
|
||||||
<ReferencePath Condition="'%(FileName)' == 'MonoMod.Iced'">
|
|
||||||
<Aliases>monomod</Aliases>
|
|
||||||
</ReferencePath>
|
|
||||||
</ItemGroup>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Needed temporarily for CI -->
|
<!-- Needed temporarily for CI -->
|
||||||
<TempVerFile>$(OutputPath)TEMP_gitver.txt</TempVerFile>
|
<TempVerFile>$(OutputPath)TEMP_gitver.txt</TempVerFile>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
using System;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
|
using Dalamud.Plugin.Internal;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
|
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||||
|
|
||||||
namespace Dalamud.Game.ClientState.Objects;
|
namespace Dalamud.Game.ClientState.Objects;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -25,18 +32,41 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
||||||
{
|
{
|
||||||
private const int ObjectTableLength = 599;
|
private const int ObjectTableLength = 599;
|
||||||
|
|
||||||
private readonly ClientStateAddressResolver address;
|
private readonly ClientState clientState;
|
||||||
|
private readonly CachedEntry[] cachedObjectTable = new CachedEntry[ObjectTableLength];
|
||||||
|
|
||||||
|
private readonly ObjectPool<Enumerator> multiThreadedEnumerators =
|
||||||
|
new DefaultObjectPoolProvider().Create<Enumerator>();
|
||||||
|
|
||||||
|
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
|
||||||
|
|
||||||
|
private long nextMultithreadedUsageWarnTime;
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
[ServiceManager.ServiceConstructor]
|
||||||
private ObjectTable(ClientState clientState)
|
private unsafe ObjectTable(ClientState clientState)
|
||||||
{
|
{
|
||||||
this.address = clientState.AddressResolver;
|
this.clientState = clientState;
|
||||||
|
|
||||||
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
|
var nativeObjectTableAddress = (CSGameObject**)this.clientState.AddressResolver.ObjectTable;
|
||||||
|
for (var i = 0; i < this.cachedObjectTable.Length; i++)
|
||||||
|
this.cachedObjectTable[i] = new(nativeObjectTableAddress, i);
|
||||||
|
|
||||||
|
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
|
||||||
|
this.frameworkThreadEnumerators[i] = new(this, i);
|
||||||
|
|
||||||
|
Log.Verbose($"Object table address 0x{this.clientState.AddressResolver.ObjectTable.ToInt64():X}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IntPtr Address => this.address.ObjectTable;
|
public nint Address
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
_ = this.WarnMultithreadedUsage();
|
||||||
|
|
||||||
|
return this.clientState.AddressResolver.ObjectTable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public int Length => ObjectTableLength;
|
public int Length => ObjectTableLength;
|
||||||
|
|
@ -46,50 +76,49 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var address = this.GetObjectAddress(index);
|
_ = this.WarnMultithreadedUsage();
|
||||||
return this.CreateObjectReference(address);
|
|
||||||
|
return index is >= ObjectTableLength or < 0 ? null : this.cachedObjectTable[index].Update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public GameObject? SearchById(ulong objectId)
|
public GameObject? SearchById(ulong objectId)
|
||||||
{
|
{
|
||||||
|
_ = this.WarnMultithreadedUsage();
|
||||||
|
|
||||||
if (objectId is GameObject.InvalidGameObjectId or 0)
|
if (objectId is GameObject.InvalidGameObjectId or 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
foreach (var obj in this)
|
foreach (var e in this.cachedObjectTable)
|
||||||
{
|
{
|
||||||
if (obj == null)
|
if (e.Update() is { } o && o.ObjectId == objectId)
|
||||||
continue;
|
return o;
|
||||||
|
|
||||||
if (obj.ObjectId == objectId)
|
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public unsafe IntPtr GetObjectAddress(int index)
|
public unsafe nint GetObjectAddress(int index)
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= ObjectTableLength)
|
_ = this.WarnMultithreadedUsage();
|
||||||
return IntPtr.Zero;
|
|
||||||
|
|
||||||
return *(IntPtr*)(this.address.ObjectTable + (8 * index));
|
return index is < 0 or >= ObjectTableLength ? nint.Zero : (nint)this.cachedObjectTable[index].Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public unsafe GameObject? CreateObjectReference(IntPtr address)
|
public unsafe GameObject? CreateObjectReference(nint address)
|
||||||
{
|
{
|
||||||
var clientState = Service<ClientState>.GetNullable();
|
_ = this.WarnMultithreadedUsage();
|
||||||
|
|
||||||
if (clientState == null || clientState.LocalContentId == 0)
|
if (this.clientState.LocalContentId == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (address == IntPtr.Zero)
|
if (address == nint.Zero)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address;
|
var obj = (CSGameObject*)address;
|
||||||
var objKind = (ObjectKind)obj->ObjectKind;
|
var objKind = (ObjectKind)obj->ObjectKind;
|
||||||
return objKind switch
|
return objKind switch
|
||||||
{
|
{
|
||||||
|
|
@ -104,6 +133,82 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
||||||
_ => new GameObject(address),
|
_ => new GameObject(address),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Api10ToDo("Use ThreadSafety.AssertMainThread() instead of this.")]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private bool WarnMultithreadedUsage()
|
||||||
|
{
|
||||||
|
if (ThreadSafety.IsMainThread)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var n = Environment.TickCount64;
|
||||||
|
if (this.nextMultithreadedUsageWarnTime < n)
|
||||||
|
{
|
||||||
|
this.nextMultithreadedUsageWarnTime = n + 30000;
|
||||||
|
|
||||||
|
Log.Warning(
|
||||||
|
"{plugin} is accessing {objectTable} outside the main thread. This is deprecated.",
|
||||||
|
Service<PluginManager>.Get().FindCallingPlugin()?.Name ?? "<unknown plugin>",
|
||||||
|
nameof(ObjectTable));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stores an object table entry, with preallocated concrete types.</summary>
|
||||||
|
internal readonly unsafe struct CachedEntry
|
||||||
|
{
|
||||||
|
private readonly CSGameObject** gameObjectPtrPtr;
|
||||||
|
private readonly PlayerCharacter playerCharacter;
|
||||||
|
private readonly BattleNpc battleNpc;
|
||||||
|
private readonly Npc npc;
|
||||||
|
private readonly EventObj eventObj;
|
||||||
|
private readonly GameObject gameObject;
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the <see cref="CachedEntry"/> struct.</summary>
|
||||||
|
/// <param name="ownerTable">The object table that this entry should be pointing to.</param>
|
||||||
|
/// <param name="slot">The slot index inside the table.</param>
|
||||||
|
public CachedEntry(CSGameObject** ownerTable, int slot)
|
||||||
|
{
|
||||||
|
this.gameObjectPtrPtr = ownerTable + slot;
|
||||||
|
this.playerCharacter = new(nint.Zero);
|
||||||
|
this.battleNpc = new(nint.Zero);
|
||||||
|
this.npc = new(nint.Zero);
|
||||||
|
this.eventObj = new(nint.Zero);
|
||||||
|
this.gameObject = new(nint.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the address of the underlying native object. May be null.</summary>
|
||||||
|
public CSGameObject* Address
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get => *this.gameObjectPtrPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Updates and gets the wrapped game object pointed by this struct.</summary>
|
||||||
|
/// <returns>The pointed object, or <c>null</c> if no object exists at that slot.</returns>
|
||||||
|
public GameObject? Update()
|
||||||
|
{
|
||||||
|
var address = this.Address;
|
||||||
|
if (address is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var activeObject = (ObjectKind)address->ObjectKind switch
|
||||||
|
{
|
||||||
|
ObjectKind.Player => this.playerCharacter,
|
||||||
|
ObjectKind.BattleNpc => this.battleNpc,
|
||||||
|
ObjectKind.EventNpc => this.npc,
|
||||||
|
ObjectKind.Retainer => this.npc,
|
||||||
|
ObjectKind.EventObj => this.eventObj,
|
||||||
|
ObjectKind.Companion => this.npc,
|
||||||
|
ObjectKind.MountType => this.npc,
|
||||||
|
ObjectKind.Ornament => this.npc,
|
||||||
|
_ => this.gameObject,
|
||||||
|
};
|
||||||
|
activeObject.Address = (nint)address;
|
||||||
|
return activeObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -117,17 +222,90 @@ internal sealed partial class ObjectTable
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerator<GameObject> GetEnumerator()
|
public IEnumerator<GameObject> GetEnumerator()
|
||||||
{
|
{
|
||||||
for (var i = 0; i < ObjectTableLength; i++)
|
// If something's trying to enumerate outside the framework thread, we use the ObjectPool.
|
||||||
|
if (this.WarnMultithreadedUsage())
|
||||||
{
|
{
|
||||||
var obj = this[i];
|
// let's not
|
||||||
|
var e = this.multiThreadedEnumerators.Get();
|
||||||
if (obj == null)
|
e.InitializeForPooledObjects(this);
|
||||||
continue;
|
return e;
|
||||||
|
|
||||||
yield return obj;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||||
|
|
||||||
|
private sealed class Enumerator : IEnumerator<GameObject>, IResettable
|
||||||
|
{
|
||||||
|
private readonly int slotId;
|
||||||
|
private ObjectTable? owner;
|
||||||
|
|
||||||
|
private int index = -1;
|
||||||
|
|
||||||
|
public Enumerator() => this.slotId = -1;
|
||||||
|
|
||||||
|
public Enumerator(ObjectTable owner, int slotId)
|
||||||
|
{
|
||||||
|
this.owner = owner;
|
||||||
|
this.slotId = slotId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameObject Current { get; private set; } = null!;
|
||||||
|
|
||||||
|
object IEnumerator.Current => this.Current;
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (this.index == ObjectTableLength)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var cache = this.owner!.cachedObjectTable.AsSpan();
|
||||||
|
for (this.index++; this.index < ObjectTableLength; this.index++)
|
||||||
|
{
|
||||||
|
if (cache[this.index].Update() is { } ao)
|
||||||
|
{
|
||||||
|
this.Current = ao;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializeForPooledObjects(ObjectTable ot) => this.owner = ot;
|
||||||
|
|
||||||
|
public void Reset() => this.index = -1;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (this.owner is not { } o)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.slotId == -1)
|
||||||
|
o.multiThreadedEnumerators.Return(this);
|
||||||
|
else
|
||||||
|
o.frameworkThreadEnumerators[this.slotId] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryReset()
|
||||||
|
{
|
||||||
|
this.Reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public unsafe partial class GameObject : IEquatable<GameObject>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the address of the game object in memory.
|
/// Gets the address of the game object in memory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IntPtr Address { get; }
|
public IntPtr Address { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the Dalamud instance.
|
/// Gets the Dalamud instance.
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ namespace Dalamud.Game;
|
||||||
internal sealed class Framework : IInternalDisposableService, IFramework
|
internal sealed class Framework : IInternalDisposableService, IFramework
|
||||||
{
|
{
|
||||||
private static readonly ModuleLog Log = new("Framework");
|
private static readonly ModuleLog Log = new("Framework");
|
||||||
|
|
||||||
private static readonly Stopwatch StatsStopwatch = new();
|
private static readonly Stopwatch StatsStopwatch = new();
|
||||||
|
|
||||||
private readonly GameLifecycle lifecycle;
|
private readonly GameLifecycle lifecycle;
|
||||||
|
|
||||||
private readonly Stopwatch updateStopwatch = new();
|
private readonly Stopwatch updateStopwatch = new();
|
||||||
|
|
@ -87,6 +87,11 @@ internal sealed class Framework : IInternalDisposableService, IFramework
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event IFramework.OnUpdateDelegate? Update;
|
public event IFramework.OnUpdateDelegate? Update;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes during FrameworkUpdate before all <see cref="Update"/> delegates.
|
||||||
|
/// </summary>
|
||||||
|
internal event IFramework.OnUpdateDelegate BeforeUpdate;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the collection of stats is enabled.
|
/// Gets or sets a value indicating whether the collection of stats is enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -333,7 +338,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
|
||||||
this.updateStopwatch.Reset();
|
this.updateStopwatch.Reset();
|
||||||
StatsStopwatch.Reset();
|
StatsStopwatch.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a update time to the stats history.
|
/// Adds a update time to the stats history.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -360,7 +365,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
|
||||||
internal void ProfileAndInvoke(IFramework.OnUpdateDelegate? eventDelegate, IFramework frameworkInstance)
|
internal void ProfileAndInvoke(IFramework.OnUpdateDelegate? eventDelegate, IFramework frameworkInstance)
|
||||||
{
|
{
|
||||||
if (eventDelegate is null) return;
|
if (eventDelegate is null) return;
|
||||||
|
|
||||||
var invokeList = eventDelegate.GetInvocationList();
|
var invokeList = eventDelegate.GetInvocationList();
|
||||||
|
|
||||||
// Individually invoke OnUpdate handlers and time them.
|
// Individually invoke OnUpdate handlers and time them.
|
||||||
|
|
@ -392,6 +397,8 @@ internal sealed class Framework : IInternalDisposableService, IFramework
|
||||||
|
|
||||||
ThreadSafety.MarkMainThread();
|
ThreadSafety.MarkMainThread();
|
||||||
|
|
||||||
|
this.BeforeUpdate?.InvokeSafely(this);
|
||||||
|
|
||||||
this.hitchDetector.Start();
|
this.hitchDetector.Start();
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -476,7 +483,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
|
||||||
|
|
||||||
this.hitchDetector.Stop();
|
this.hitchDetector.Stop();
|
||||||
|
|
||||||
original:
|
original:
|
||||||
return this.updateHook.OriginalDisposeSafe(framework);
|
return this.updateHook.OriginalDisposeSafe(framework);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -529,19 +536,19 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public DateTime LastUpdate => this.frameworkService.LastUpdate;
|
public DateTime LastUpdate => this.frameworkService.LastUpdate;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC;
|
public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta;
|
public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool IsInFrameworkUpdateThread => this.frameworkService.IsInFrameworkUpdateThread;
|
public bool IsInFrameworkUpdateThread => this.frameworkService.IsInFrameworkUpdateThread;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading;
|
public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
void IInternalDisposableService.DisposeService()
|
void IInternalDisposableService.DisposeService()
|
||||||
{
|
{
|
||||||
|
|
@ -576,27 +583,27 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<T> RunOnFrameworkThread<T>(Func<T> func)
|
public Task<T> RunOnFrameworkThread<T>(Func<T> func)
|
||||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task RunOnFrameworkThread(Action action)
|
public Task RunOnFrameworkThread(Action action)
|
||||||
=> this.frameworkService.RunOnFrameworkThread(action);
|
=> this.frameworkService.RunOnFrameworkThread(action);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<T> RunOnFrameworkThread<T>(Func<Task<T>> func)
|
public Task<T> RunOnFrameworkThread<T>(Func<Task<T>> func)
|
||||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task RunOnFrameworkThread(Func<Task> func)
|
public Task RunOnFrameworkThread(Func<Task> func)
|
||||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<T> RunOnTick<T>(Func<T> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
public Task<T> RunOnTick<T>(Func<T> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||||
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
|
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||||
=> this.frameworkService.RunOnTick(action, delay, delayTicks, cancellationToken);
|
=> this.frameworkService.RunOnTick(action, delay, delayTicks, cancellationToken);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<T> RunOnTick<T>(Func<Task<T>> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
public Task<T> RunOnTick<T>(Func<Task<T>> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||||
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
|
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
|
using Dalamud.Configuration.Internal;
|
||||||
using Dalamud.Interface.Internal;
|
using Dalamud.Interface.Internal;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
|
|
@ -20,8 +21,8 @@ internal sealed partial class ActiveNotification
|
||||||
var opacity =
|
var opacity =
|
||||||
Math.Clamp(
|
Math.Clamp(
|
||||||
(float)(this.hideEasing.IsRunning
|
(float)(this.hideEasing.IsRunning
|
||||||
? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value)
|
? (this.hideEasing.IsDone || ReducedMotions ? 0 : 1f - this.hideEasing.Value)
|
||||||
: (this.showEasing.IsDone ? 1 : this.showEasing.Value)),
|
: (this.showEasing.IsDone || ReducedMotions ? 1 : this.showEasing.Value)),
|
||||||
0f,
|
0f,
|
||||||
1f);
|
1f);
|
||||||
if (opacity <= 0)
|
if (opacity <= 0)
|
||||||
|
|
@ -97,24 +98,25 @@ internal sealed partial class ActiveNotification
|
||||||
this.lastInterestTime = DateTime.Now;
|
this.lastInterestTime = DateTime.Now;
|
||||||
|
|
||||||
this.DrawWindowBackgroundProgressBar();
|
this.DrawWindowBackgroundProgressBar();
|
||||||
this.DrawTopBar(width, actionWindowHeight, isHovered);
|
this.DrawTopBar(width, actionWindowHeight, isHovered, warrantsExtension);
|
||||||
if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning)
|
if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning)
|
||||||
{
|
{
|
||||||
this.DrawContentAndActions(width, actionWindowHeight);
|
this.DrawContentAndActions(width, actionWindowHeight);
|
||||||
}
|
}
|
||||||
else if (this.expandoEasing.IsRunning)
|
else if (this.expandoEasing.IsRunning)
|
||||||
{
|
{
|
||||||
|
var easedValue = ReducedMotions ? 1f : (float)this.expandoEasing.Value;
|
||||||
if (this.underlyingNotification.Minimized)
|
if (this.underlyingNotification.Minimized)
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value));
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - easedValue));
|
||||||
else
|
else
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value);
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * easedValue);
|
||||||
this.DrawContentAndActions(width, actionWindowHeight);
|
this.DrawContentAndActions(width, actionWindowHeight);
|
||||||
ImGui.PopStyleVar();
|
ImGui.PopStyleVar();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFocused)
|
if (isFocused)
|
||||||
this.DrawFocusIndicator();
|
this.DrawFocusIndicator();
|
||||||
this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension);
|
this.DrawExpiryBar(warrantsExtension);
|
||||||
|
|
||||||
if (ImGui.IsWindowHovered())
|
if (ImGui.IsWindowHovered())
|
||||||
{
|
{
|
||||||
|
|
@ -184,24 +186,36 @@ internal sealed partial class ActiveNotification
|
||||||
|
|
||||||
private void DrawWindowBackgroundProgressBar()
|
private void DrawWindowBackgroundProgressBar()
|
||||||
{
|
{
|
||||||
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
|
var elapsed = 0f;
|
||||||
NotificationConstants.ProgressWaveLoopDuration) /
|
var colorElapsed = 0f;
|
||||||
NotificationConstants.ProgressWaveLoopDuration);
|
float progress;
|
||||||
elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio;
|
|
||||||
|
|
||||||
var colorElapsed =
|
if (ReducedMotions)
|
||||||
elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
|
{
|
||||||
? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
|
progress = this.Progress;
|
||||||
: ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) /
|
}
|
||||||
NotificationConstants.ProgressWaveLoopMaxColorTimeRatio;
|
else
|
||||||
|
{
|
||||||
|
progress = Math.Clamp(this.ProgressEased, 0f, 1f);
|
||||||
|
|
||||||
elapsed = Math.Clamp(elapsed, 0f, 1f);
|
elapsed =
|
||||||
colorElapsed = Math.Clamp(colorElapsed, 0f, 1f);
|
(float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
|
||||||
colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f));
|
NotificationConstants.ProgressWaveLoopDuration) /
|
||||||
|
NotificationConstants.ProgressWaveLoopDuration);
|
||||||
|
elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio;
|
||||||
|
|
||||||
var progress = Math.Clamp(this.ProgressEased, 0f, 1f);
|
colorElapsed = elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
|
||||||
if (progress >= 1f)
|
? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
|
||||||
elapsed = colorElapsed = 0f;
|
: ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) /
|
||||||
|
NotificationConstants.ProgressWaveLoopMaxColorTimeRatio;
|
||||||
|
|
||||||
|
elapsed = Math.Clamp(elapsed, 0f, 1f);
|
||||||
|
colorElapsed = Math.Clamp(colorElapsed, 0f, 1f);
|
||||||
|
colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f));
|
||||||
|
|
||||||
|
if (progress >= 1f)
|
||||||
|
elapsed = colorElapsed = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
var windowPos = ImGui.GetWindowPos();
|
var windowPos = ImGui.GetWindowPos();
|
||||||
var windowSize = ImGui.GetWindowSize();
|
var windowSize = ImGui.GetWindowSize();
|
||||||
|
|
@ -240,7 +254,7 @@ internal sealed partial class ActiveNotification
|
||||||
ImGui.PopClipRect();
|
ImGui.PopClipRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawTopBar(float width, float height, bool drawActionButtons)
|
private void DrawTopBar(float width, float height, bool drawActionButtons, bool warrantsExtension)
|
||||||
{
|
{
|
||||||
var windowPos = ImGui.GetWindowPos();
|
var windowPos = ImGui.GetWindowPos();
|
||||||
var windowSize = ImGui.GetWindowSize();
|
var windowSize = ImGui.GetWindowSize();
|
||||||
|
|
@ -249,6 +263,10 @@ internal sealed partial class ActiveNotification
|
||||||
using (Service<InterfaceManager>.Get().IconFontHandle?.Push())
|
using (Service<InterfaceManager>.Get().IconFontHandle?.Push())
|
||||||
{
|
{
|
||||||
ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false);
|
ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false);
|
||||||
|
|
||||||
|
if (!drawActionButtons)
|
||||||
|
this.DrawExpiryPie(warrantsExtension, new(width - height, 0), new(height));
|
||||||
|
|
||||||
if (this.UserDismissable)
|
if (this.UserDismissable)
|
||||||
{
|
{
|
||||||
if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons))
|
if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons))
|
||||||
|
|
@ -272,7 +290,7 @@ internal sealed partial class ActiveNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
float relativeOpacity;
|
float relativeOpacity;
|
||||||
if (this.expandoEasing.IsRunning)
|
if (this.expandoEasing.IsRunning && !ReducedMotions)
|
||||||
{
|
{
|
||||||
relativeOpacity =
|
relativeOpacity =
|
||||||
this.underlyingNotification.Minimized
|
this.underlyingNotification.Minimized
|
||||||
|
|
@ -297,36 +315,35 @@ internal sealed partial class ActiveNotification
|
||||||
ImGui.TextUnformatted(
|
ImGui.TextUnformatted(
|
||||||
ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)
|
ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)
|
||||||
? this.CreatedAt.LocAbsolute()
|
? this.CreatedAt.LocAbsolute()
|
||||||
: this.CreatedAt.LocRelativePastLong());
|
: ReducedMotions
|
||||||
|
? this.CreatedAt.LocRelativePastLong(TimeSpan.FromSeconds(15))
|
||||||
|
: this.CreatedAt.LocRelativePastLong(TimeSpan.FromSeconds(5)));
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
ImGui.PopStyleVar();
|
ImGui.PopStyleVar();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relativeOpacity < 1)
|
if (relativeOpacity < 1)
|
||||||
{
|
{
|
||||||
rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity));
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity));
|
||||||
|
|
||||||
var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding);
|
var agoText =
|
||||||
this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding)));
|
ReducedMotions
|
||||||
|
? this.CreatedAt.LocRelativePastShort(TimeSpan.FromSeconds(15))
|
||||||
ltOffset.X = height;
|
: this.CreatedAt.LocRelativePastShort(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
var agoText = this.CreatedAt.LocRelativePastShort();
|
|
||||||
var agoSize = ImGui.CalcTextSize(agoText);
|
var agoSize = ImGui.CalcTextSize(agoText);
|
||||||
rtOffset.X -= agoSize.X;
|
ImGui.SetCursorPos(new(width - ((height + agoSize.X) / 2f), NotificationConstants.ScaledWindowPadding));
|
||||||
ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding });
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
|
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
|
||||||
ImGui.TextUnformatted(agoText);
|
ImGui.TextUnformatted(agoText);
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
|
|
||||||
rtOffset.X -= NotificationConstants.ScaledWindowPadding;
|
this.DrawIcon(
|
||||||
|
new(NotificationConstants.ScaledWindowPadding),
|
||||||
|
new(height - (2 * NotificationConstants.ScaledWindowPadding)));
|
||||||
ImGui.PushClipRect(
|
ImGui.PushClipRect(
|
||||||
windowPos + ltOffset with { Y = 0 },
|
windowPos + new Vector2(height, 0),
|
||||||
windowPos + rtOffset with { Y = height },
|
windowPos + new Vector2(width - height, height),
|
||||||
true);
|
true);
|
||||||
ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding });
|
ImGui.SetCursorPos(new(height, NotificationConstants.ScaledWindowPadding));
|
||||||
ImGui.TextUnformatted(this.EffectiveMinimizedText);
|
ImGui.TextUnformatted(this.EffectiveMinimizedText);
|
||||||
ImGui.PopClipRect();
|
ImGui.PopClipRect();
|
||||||
|
|
||||||
|
|
@ -437,12 +454,95 @@ internal sealed partial class ActiveNotification
|
||||||
ImGui.PopTextWrapPos();
|
ImGui.PopTextWrapPos();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension)
|
private void DrawExpiryPie(bool warrantsExtension, Vector2 offset, Vector2 size)
|
||||||
{
|
{
|
||||||
|
if (!Service<DalamudConfiguration>.Get().ReduceMotions ?? false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// circle here; 0 means 0deg; 1 means 360deg
|
||||||
|
float fillStartCw, fillEndCw;
|
||||||
|
if (this.DismissReason is not null)
|
||||||
|
{
|
||||||
|
fillStartCw = fillEndCw = 0f;
|
||||||
|
}
|
||||||
|
else if (warrantsExtension)
|
||||||
|
{
|
||||||
|
fillStartCw = fillEndCw = 0f;
|
||||||
|
}
|
||||||
|
else if (this.EffectiveExpiry == DateTime.MaxValue)
|
||||||
|
{
|
||||||
|
if (this.ShowIndeterminateIfNoExpiry)
|
||||||
|
{
|
||||||
|
// draw
|
||||||
|
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
|
||||||
|
NotificationConstants.IndeterminatePieLoopDuration) /
|
||||||
|
NotificationConstants.IndeterminatePieLoopDuration);
|
||||||
|
fillStartCw = elapsed;
|
||||||
|
fillEndCw = elapsed + 0.2f + (MathF.Sin(elapsed * MathF.PI) * 0.2f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// do not draw
|
||||||
|
fillStartCw = fillEndCw = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fillStartCw = 1f - (float)((this.EffectiveExpiry - DateTime.Now).TotalMilliseconds /
|
||||||
|
(this.EffectiveExpiry - this.lastInterestTime).TotalMilliseconds);
|
||||||
|
fillEndCw = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fillStartCw > fillEndCw)
|
||||||
|
(fillStartCw, fillEndCw) = (fillEndCw, fillStartCw);
|
||||||
|
|
||||||
|
if (fillStartCw == 0 && fillEndCw == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var radius = Math.Min(size.X, size.Y) / 3f;
|
||||||
|
var ifrom = fillStartCw * MathF.PI * 2;
|
||||||
|
var ito = fillEndCw * MathF.PI * 2;
|
||||||
|
|
||||||
|
var nseg = MathF.Ceiling(2 * MathF.PI * radius);
|
||||||
|
var step = (MathF.PI * 2) / nseg;
|
||||||
|
|
||||||
|
var center = ImGui.GetWindowPos() + offset + (size / 2);
|
||||||
|
var color = ImGui.GetColorU32(this.Type.ToColor() * new Vector4(1, 1, 1, 0.2f));
|
||||||
|
|
||||||
|
var prevOff = center + (radius * new Vector2(MathF.Sin(ifrom), -MathF.Cos(ifrom)));
|
||||||
|
Span<Vector2> verts = stackalloc Vector2[(int)MathF.Ceiling(((ito - ifrom) / step) + 3)];
|
||||||
|
var vertPtr = 0;
|
||||||
|
verts[vertPtr++] = center;
|
||||||
|
verts[vertPtr++] = prevOff;
|
||||||
|
|
||||||
|
var cur = ifrom + step;
|
||||||
|
for (; cur < ito; cur += step)
|
||||||
|
{
|
||||||
|
var curOff = center + (radius * new Vector2(MathF.Sin(cur), -MathF.Cos(cur)));
|
||||||
|
if (Vector2.DistanceSquared(prevOff, curOff) >= 1)
|
||||||
|
verts[vertPtr++] = prevOff = curOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastOff = center + (radius * new Vector2(MathF.Sin(ito), -MathF.Cos(ito)));
|
||||||
|
if (Vector2.DistanceSquared(prevOff, lastOff) >= 1)
|
||||||
|
verts[vertPtr++] = lastOff;
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var dlist = ImGui.GetWindowDrawList().NativePtr;
|
||||||
|
fixed (Vector2* pvert = verts)
|
||||||
|
ImGuiNative.ImDrawList_AddConvexPolyFilled(dlist, pvert, vertPtr, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawExpiryBar(bool warrantsExtension)
|
||||||
|
{
|
||||||
|
if (Service<DalamudConfiguration>.Get().ReduceMotions ?? false)
|
||||||
|
return;
|
||||||
|
|
||||||
float barL, barR;
|
float barL, barR;
|
||||||
if (this.DismissReason is not null)
|
if (this.DismissReason is not null)
|
||||||
{
|
{
|
||||||
var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value;
|
var v = this.hideEasing.IsDone || ReducedMotions ? 0f : 1f - (float)this.hideEasing.Value;
|
||||||
var midpoint = (this.prevProgressL + this.prevProgressR) / 2f;
|
var midpoint = (this.prevProgressL + this.prevProgressR) / 2f;
|
||||||
var length = (this.prevProgressR - this.prevProgressL) / 2f;
|
var length = (this.prevProgressR - this.prevProgressL) / 2f;
|
||||||
barL = midpoint - (length * v);
|
barL = midpoint - (length * v);
|
||||||
|
|
@ -455,7 +555,7 @@ internal sealed partial class ActiveNotification
|
||||||
this.prevProgressL = barL;
|
this.prevProgressL = barL;
|
||||||
this.prevProgressR = barR;
|
this.prevProgressR = barR;
|
||||||
}
|
}
|
||||||
else if (effectiveExpiry == DateTime.MaxValue)
|
else if (this.EffectiveExpiry == DateTime.MaxValue)
|
||||||
{
|
{
|
||||||
if (this.ShowIndeterminateIfNoExpiry)
|
if (this.ShowIndeterminateIfNoExpiry)
|
||||||
{
|
{
|
||||||
|
|
@ -477,8 +577,8 @@ internal sealed partial class ActiveNotification
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds /
|
barL = 1f - (float)((this.EffectiveExpiry - DateTime.Now).TotalMilliseconds /
|
||||||
(effectiveExpiry - this.lastInterestTime).TotalMilliseconds);
|
(this.EffectiveExpiry - this.lastInterestTime).TotalMilliseconds);
|
||||||
barR = 1f;
|
barR = 1f;
|
||||||
this.prevProgressL = barL;
|
this.prevProgressL = barL;
|
||||||
this.prevProgressR = barR;
|
this.prevProgressR = barR;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Runtime.Loader;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Dalamud.Configuration.Internal;
|
||||||
using Dalamud.Interface.Animation;
|
using Dalamud.Interface.Animation;
|
||||||
using Dalamud.Interface.Animation.EasingFunctions;
|
using Dalamud.Interface.Animation.EasingFunctions;
|
||||||
using Dalamud.Interface.Internal;
|
using Dalamud.Interface.Internal;
|
||||||
|
|
@ -187,16 +188,18 @@ internal sealed partial class ActiveNotification : IActiveNotification
|
||||||
set => this.newProgress = value;
|
set => this.newProgress = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ReducedMotions => Service<DalamudConfiguration>.Get().ReduceMotions ?? false;
|
||||||
|
|
||||||
/// <summary>Gets the eased progress.</summary>
|
/// <summary>Gets the eased progress.</summary>
|
||||||
private float ProgressEased
|
private float ProgressEased
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var underlyingProgress = this.underlyingNotification.Progress;
|
var underlyingProgress = this.underlyingNotification.Progress;
|
||||||
if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone)
|
if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone || ReducedMotions)
|
||||||
return underlyingProgress;
|
return underlyingProgress;
|
||||||
|
|
||||||
var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f);
|
var state = ReducedMotions ? 1f : Math.Clamp((float)this.progressEasing.Value, 0f, 1f);
|
||||||
return this.progressBefore + (state * (underlyingProgress - this.progressBefore));
|
return this.progressBefore + (state * (underlyingProgress - this.progressBefore));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ internal static class NotificationConstants
|
||||||
/// <summary>The duration of indeterminate progress bar loop in milliseconds.</summary>
|
/// <summary>The duration of indeterminate progress bar loop in milliseconds.</summary>
|
||||||
public const float IndeterminateProgressbarLoopDuration = 2000f;
|
public const float IndeterminateProgressbarLoopDuration = 2000f;
|
||||||
|
|
||||||
|
/// <summary>The duration of indeterminate pie loop in milliseconds.</summary>
|
||||||
|
/// <remarks>Note that this value is applicable when reduced motion configuration is on.</remarks>
|
||||||
|
public const float IndeterminatePieLoopDuration = 8000f;
|
||||||
|
|
||||||
/// <summary>The duration of the progress wave animation in milliseconds.</summary>
|
/// <summary>The duration of the progress wave animation in milliseconds.</summary>
|
||||||
public const float ProgressWaveLoopDuration = 2000f;
|
public const float ProgressWaveLoopDuration = 2000f;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ public static class NotificationUtilities
|
||||||
plugin,
|
plugin,
|
||||||
plugin.Manifest,
|
plugin.Manifest,
|
||||||
plugin.IsThirdParty,
|
plugin.IsThirdParty,
|
||||||
out var texture) || texture is null)
|
out var texture, out _) || texture is null)
|
||||||
{
|
{
|
||||||
texture = dam.GetDalamudTextureWrap(DalamudAsset.DefaultIcon);
|
texture = dam.GetDalamudTextureWrap(DalamudAsset.DefaultIcon);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ internal class PluginImageCache : IInternalDisposableService
|
||||||
private readonly CancellationTokenSource cancelToken = new();
|
private readonly CancellationTokenSource cancelToken = new();
|
||||||
private readonly Task downloadTask;
|
private readonly Task downloadTask;
|
||||||
private readonly Task loadTask;
|
private readonly Task loadTask;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, IDalamudTextureWrap?> pluginIconMap = new();
|
private readonly ConcurrentDictionary<string, LoadedIcon?> pluginIconMap = new();
|
||||||
private readonly ConcurrentDictionary<string, IDalamudTextureWrap?[]?> pluginImagesMap = new();
|
private readonly ConcurrentDictionary<string, IDalamudTextureWrap?[]?> pluginImagesMap = new();
|
||||||
private readonly DalamudAssetManager dalamudAssetManager;
|
private readonly DalamudAssetManager dalamudAssetManager;
|
||||||
|
|
||||||
|
|
@ -153,7 +153,7 @@ internal class PluginImageCache : IInternalDisposableService
|
||||||
|
|
||||||
foreach (var icon in this.pluginIconMap.Values)
|
foreach (var icon in this.pluginIconMap.Values)
|
||||||
{
|
{
|
||||||
icon?.Dispose();
|
icon?.Texture.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var images in this.pluginImagesMap.Values)
|
foreach (var images in this.pluginImagesMap.Values)
|
||||||
|
|
@ -185,10 +185,12 @@ internal class PluginImageCache : IInternalDisposableService
|
||||||
/// <param name="manifest">The plugin manifest.</param>
|
/// <param name="manifest">The plugin manifest.</param>
|
||||||
/// <param name="isThirdParty">If the plugin was third party sourced.</param>
|
/// <param name="isThirdParty">If the plugin was third party sourced.</param>
|
||||||
/// <param name="iconTexture">Cached image textures, or an empty array.</param>
|
/// <param name="iconTexture">Cached image textures, or an empty array.</param>
|
||||||
|
/// <param name="loadedSince">The time the icon was successfully downloaded.</param>
|
||||||
/// <returns>True if an entry exists, may be null if currently downloading.</returns>
|
/// <returns>True if an entry exists, may be null if currently downloading.</returns>
|
||||||
public bool TryGetIcon(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out IDalamudTextureWrap? iconTexture)
|
public bool TryGetIcon(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out IDalamudTextureWrap? iconTexture, out DateTime? loadedSince)
|
||||||
{
|
{
|
||||||
iconTexture = null;
|
iconTexture = null;
|
||||||
|
loadedSince = null;
|
||||||
|
|
||||||
if (manifest == null || manifest.InternalName == null)
|
if (manifest == null || manifest.InternalName == null)
|
||||||
{
|
{
|
||||||
|
|
@ -198,7 +200,13 @@ internal class PluginImageCache : IInternalDisposableService
|
||||||
|
|
||||||
if (!this.pluginIconMap.TryAdd(manifest.InternalName, null))
|
if (!this.pluginIconMap.TryAdd(manifest.InternalName, null))
|
||||||
{
|
{
|
||||||
iconTexture = this.pluginIconMap[manifest.InternalName];
|
var loaded = this.pluginIconMap[manifest.InternalName];
|
||||||
|
if (loaded != null)
|
||||||
|
{
|
||||||
|
iconTexture = loaded.Texture;
|
||||||
|
loadedSince = loaded.LoadedSince;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,8 +215,9 @@ internal class PluginImageCache : IInternalDisposableService
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.pluginIconMap[manifest.InternalName] =
|
var texture = await this.DownloadPluginIconAsync(plugin, manifest, isThirdParty, requestedFrame);
|
||||||
await this.DownloadPluginIconAsync(plugin, manifest, isThirdParty, requestedFrame);
|
if (texture != null)
|
||||||
|
this.pluginIconMap[manifest.InternalName] = new LoadedIcon(texture, DateTime.Now);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -690,4 +699,11 @@ internal class PluginImageCache : IInternalDisposableService
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Record for a loaded icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Texture">The texture of the icon.</param>
|
||||||
|
/// <param name="LoadedSince">The time the icon was loaded at.</param>
|
||||||
|
private record LoadedIcon(IDalamudTextureWrap Texture, DateTime LoadedSince);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1158,10 +1158,13 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
// Go through all AVAILABLE manifests, associate them with a NON-DEV local plugin, if one is available, and remove it from the pile
|
// Go through all AVAILABLE manifests, associate them with a NON-DEV local plugin, if one is available, and remove it from the pile
|
||||||
foreach (var availableManifest in this.categoryManager.GetCurrentCategoryContent(filteredAvailableManifests).Cast<RemotePluginManifest>())
|
foreach (var availableManifest in this.categoryManager.GetCurrentCategoryContent(filteredAvailableManifests).Cast<RemotePluginManifest>())
|
||||||
{
|
{
|
||||||
var plugin = this.pluginListInstalled.FirstOrDefault(plugin => plugin.Manifest.InternalName == availableManifest.InternalName && plugin.Manifest.RepoUrl == availableManifest.RepoUrl);
|
var plugin = this.pluginListInstalled
|
||||||
|
.FirstOrDefault(plugin => plugin.Manifest.InternalName == availableManifest.InternalName &&
|
||||||
|
plugin.Manifest.RepoUrl == availableManifest.RepoUrl &&
|
||||||
|
!plugin.IsDev);
|
||||||
|
|
||||||
// We "consumed" this plugin from the pile and remove it.
|
// We "consumed" this plugin from the pile and remove it.
|
||||||
if (plugin != null && !plugin.IsDev)
|
if (plugin != null)
|
||||||
{
|
{
|
||||||
installedPlugins.Remove(plugin);
|
installedPlugins.Remove(plugin);
|
||||||
proxies.Add(new PluginInstallerAvailablePluginProxy(null, plugin));
|
proxies.Add(new PluginInstallerAvailablePluginProxy(null, plugin));
|
||||||
|
|
@ -1808,26 +1811,35 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
var iconSize = ImGuiHelpers.ScaledVector2(64, 64);
|
var iconSize = ImGuiHelpers.ScaledVector2(64, 64);
|
||||||
var cursorBeforeImage = ImGui.GetCursorPos();
|
var cursorBeforeImage = ImGui.GetCursorPos();
|
||||||
var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos();
|
var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos();
|
||||||
|
|
||||||
|
var overlayAlpha = 1.0f;
|
||||||
if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize))
|
if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize))
|
||||||
{
|
{
|
||||||
var iconTex = this.imageCache.DefaultIcon;
|
var iconTex = this.imageCache.DefaultIcon;
|
||||||
var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, isThirdParty, out var cachedIconTex);
|
var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, isThirdParty, out var cachedIconTex, out var loadedSince);
|
||||||
if (hasIcon && cachedIconTex != null)
|
if (hasIcon && cachedIconTex != null)
|
||||||
{
|
{
|
||||||
iconTex = cachedIconTex;
|
iconTex = cachedIconTex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const float fadeTime = 0.3f;
|
||||||
|
var iconAlpha = 1f;
|
||||||
|
|
||||||
if (pluginDisabled || installableOutdated)
|
if (loadedSince.HasValue)
|
||||||
{
|
{
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f);
|
float EaseOutCubic(float t) => 1 - MathF.Pow(1 - t, 3);
|
||||||
|
|
||||||
|
var secondsSinceLoad = (float)DateTime.Now.Subtract(loadedSince.Value).TotalSeconds;
|
||||||
|
var fadeTo = pluginDisabled || installableOutdated ? 0.4f : 1f;
|
||||||
|
|
||||||
|
float Interp(float to) => Math.Clamp(EaseOutCubic(Math.Min(secondsSinceLoad, fadeTime) / fadeTime) * to, 0, 1);
|
||||||
|
iconAlpha = Interp(fadeTo);
|
||||||
|
overlayAlpha = Interp(1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, iconAlpha);
|
||||||
ImGui.Image(iconTex.ImGuiHandle, iconSize);
|
ImGui.Image(iconTex.ImGuiHandle, iconSize);
|
||||||
|
ImGui.PopStyleVar();
|
||||||
if (pluginDisabled || installableOutdated)
|
|
||||||
{
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPos(cursorBeforeImage);
|
ImGui.SetCursorPos(cursorBeforeImage);
|
||||||
|
|
@ -1835,6 +1847,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
|
|
||||||
var isLoaded = plugin is { IsLoaded: true };
|
var isLoaded = plugin is { IsLoaded: true };
|
||||||
|
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, overlayAlpha);
|
||||||
if (updateAvailable)
|
if (updateAvailable)
|
||||||
ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize);
|
ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize);
|
||||||
else if ((trouble && !pluginDisabled) || isOrphan)
|
else if ((trouble && !pluginDisabled) || isOrphan)
|
||||||
|
|
@ -1853,6 +1866,8 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
ImGui.Image(this.imageCache.InstalledIcon.ImGuiHandle, iconSize);
|
ImGui.Image(this.imageCache.InstalledIcon.ImGuiHandle, iconSize);
|
||||||
else
|
else
|
||||||
ImGui.Dummy(iconSize);
|
ImGui.Dummy(iconSize);
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
@ -2016,7 +2031,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
if (log is PluginChangelogEntry pluginLog)
|
if (log is PluginChangelogEntry pluginLog)
|
||||||
{
|
{
|
||||||
icon = this.imageCache.DefaultIcon;
|
icon = this.imageCache.DefaultIcon;
|
||||||
var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.IsThirdParty, out var cachedIconTex);
|
var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.IsThirdParty, out var cachedIconTex, out _);
|
||||||
if (hasIcon && cachedIconTex != null)
|
if (hasIcon && cachedIconTex != null)
|
||||||
{
|
{
|
||||||
icon = cachedIconTex;
|
icon = cachedIconTex;
|
||||||
|
|
|
||||||
|
|
@ -437,7 +437,7 @@ internal class ProfileManagerWidget
|
||||||
if (pmPlugin != null)
|
if (pmPlugin != null)
|
||||||
{
|
{
|
||||||
var cursorBeforeIcon = ImGui.GetCursorPos();
|
var cursorBeforeIcon = ImGui.GetCursorPos();
|
||||||
pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon);
|
pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon, out _);
|
||||||
icon ??= pic.DefaultIcon;
|
icon ??= pic.DefaultIcon;
|
||||||
|
|
||||||
ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight));
|
ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight));
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ internal class ContextMenuAgingStep : IAgingStep
|
||||||
|
|
||||||
private void OnMenuOpened(MenuOpenedArgs args)
|
private void OnMenuOpened(MenuOpenedArgs args)
|
||||||
{
|
{
|
||||||
LogMenuOpened(args);
|
this.LogMenuOpened(args);
|
||||||
|
|
||||||
switch (this.currentSubStep)
|
switch (this.currentSubStep)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,12 @@ public class SettingsTabLook : SettingsTab
|
||||||
Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."),
|
Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."),
|
||||||
c => c.PluginInstallerOpen == PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins,
|
c => c.PluginInstallerOpen == PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins,
|
||||||
(v, c) => c.PluginInstallerOpen = v ? PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins : PluginInstallerWindow.PluginInstallerOpenKind.AllPlugins),
|
(v, c) => c.PluginInstallerOpen = v ? PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins : PluginInstallerWindow.PluginInstallerOpenKind.AllPlugins),
|
||||||
|
|
||||||
|
new SettingsEntry<bool>(
|
||||||
|
Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"),
|
||||||
|
Loc.Localize("DalamudSettingReducedMotion", "This will suppress certain animations from Dalamud, such as the notification popup."),
|
||||||
|
c => c.ReduceMotions ?? false,
|
||||||
|
(v, c) => c.ReduceMotions = v),
|
||||||
};
|
};
|
||||||
|
|
||||||
public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel");
|
public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel");
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ public class StyleEditorWindow : Window
|
||||||
var workStyle = config.SavedStyles[this.currentSel];
|
var workStyle = config.SavedStyles[this.currentSel];
|
||||||
workStyle.BuiltInColors ??= StyleModelV1.DalamudStandard.BuiltInColors;
|
workStyle.BuiltInColors ??= StyleModelV1.DalamudStandard.BuiltInColors;
|
||||||
|
|
||||||
|
var isBuiltinStyle = this.currentSel < 2;
|
||||||
var appliedThisFrame = false;
|
var appliedThisFrame = false;
|
||||||
|
|
||||||
var styleAry = config.SavedStyles.Select(x => x.Name).ToArray();
|
var styleAry = config.SavedStyles.Select(x => x.Name).ToArray();
|
||||||
|
|
@ -111,6 +112,9 @@ public class StyleEditorWindow : Window
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if (isBuiltinStyle)
|
||||||
|
ImGui.BeginDisabled();
|
||||||
|
|
||||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash) && this.currentSel != 0)
|
if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash) && this.currentSel != 0)
|
||||||
{
|
{
|
||||||
this.currentSel--;
|
this.currentSel--;
|
||||||
|
|
@ -155,6 +159,9 @@ public class StyleEditorWindow : Window
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip(Loc.Localize("StyleEditorCopy", "Copy style to clipboard for sharing"));
|
ImGui.SetTooltip(Loc.Localize("StyleEditorCopy", "Copy style to clipboard for sharing"));
|
||||||
|
|
||||||
|
if (isBuiltinStyle)
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
|
@ -196,7 +203,7 @@ public class StyleEditorWindow : Window
|
||||||
|
|
||||||
ImGui.PushItemWidth(ImGui.GetWindowWidth() * 0.50f);
|
ImGui.PushItemWidth(ImGui.GetWindowWidth() * 0.50f);
|
||||||
|
|
||||||
if (this.currentSel < 2)
|
if (isBuiltinStyle)
|
||||||
{
|
{
|
||||||
ImGui.TextColored(ImGuiColors.DalamudRed, Loc.Localize("StyleEditorNotAllowed", "You cannot edit built-in styles. Please add a new style first."));
|
ImGui.TextColored(ImGuiColors.DalamudRed, Loc.Localize("StyleEditorNotAllowed", "You cannot edit built-in styles. Please add a new style first."));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
@ -132,7 +133,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance = FormatterServices.GetUninitializedObject(objectType);
|
var instance = RuntimeHelpers.GetUninitializedObject(objectType);
|
||||||
|
|
||||||
if (!await this.InjectProperties(instance, scopedObjects, scope))
|
if (!await this.InjectProperties(instance, scopedObjects, scope))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ internal class TaskTracker : IInternalDisposableService
|
||||||
[ServiceManager.ServiceDependency]
|
[ServiceManager.ServiceDependency]
|
||||||
private readonly Framework framework = Service<Framework>.Get();
|
private readonly Framework framework = Service<Framework>.Get();
|
||||||
|
|
||||||
private MonoMod.RuntimeDetour.Hook? scheduleAndStartHook;
|
// NET8 CHORE
|
||||||
|
// private MonoMod.RuntimeDetour.Hook? scheduleAndStartHook;
|
||||||
private bool enabled = false;
|
private bool enabled = false;
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
[ServiceManager.ServiceConstructor]
|
||||||
|
|
@ -121,7 +122,8 @@ internal class TaskTracker : IInternalDisposableService
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
void IInternalDisposableService.DisposeService()
|
void IInternalDisposableService.DisposeService()
|
||||||
{
|
{
|
||||||
this.scheduleAndStartHook?.Dispose();
|
// NET8 CHORE
|
||||||
|
// this.scheduleAndStartHook?.Dispose();
|
||||||
|
|
||||||
this.framework.Update -= this.FrameworkOnUpdate;
|
this.framework.Update -= this.FrameworkOnUpdate;
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +172,8 @@ internal class TaskTracker : IInternalDisposableService
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scheduleAndStartHook = new MonoMod.RuntimeDetour.Hook(targetMethod, patchMethod);
|
// NET8 CHORE
|
||||||
|
// this.scheduleAndStartHook = new MonoMod.RuntimeDetour.Hook(targetMethod, patchMethod);
|
||||||
|
|
||||||
Log.Information("AddToActiveTasks Hooked!");
|
Log.Information("AddToActiveTasks Hooked!");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,4 @@ public class MemoryAllocationException : MemoryException
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="MemoryAllocationException"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="info">The object that holds the serialized data about the exception being thrown.</param>
|
|
||||||
/// <param name="context">The object that contains contextual information about the source or destination.</param>
|
|
||||||
protected MemoryAllocationException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,4 @@ public abstract class MemoryException : Exception
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="MemoryException"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="info">The object that holds the serialized data about the exception being thrown.</param>
|
|
||||||
/// <param name="context">The object that contains contextual information about the source or destination.</param>
|
|
||||||
protected MemoryException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,4 @@ public class MemoryPermissionException : MemoryException
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="MemoryPermissionException"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="info">The object that holds the serialized data about the exception being thrown.</param>
|
|
||||||
/// <param name="context">The object that contains contextual information about the source or destination.</param>
|
|
||||||
protected MemoryPermissionException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,4 @@ public class MemoryReadException : MemoryException
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="MemoryReadException"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="info">The object that holds the serialized data about the exception being thrown.</param>
|
|
||||||
/// <param name="context">The object that contains contextual information about the source or destination.</param>
|
|
||||||
protected MemoryReadException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,4 @@ public class MemoryWriteException : MemoryException
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="MemoryWriteException"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="info">The object that holds the serialized data about the exception being thrown.</param>
|
|
||||||
/// <param name="context">The object that contains contextual information about the source or destination.</param>
|
|
||||||
protected MemoryWriteException(SerializationInfo info, StreamingContext context)
|
|
||||||
: base(info, context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -826,6 +826,27 @@ internal static partial class NativeFunctions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint Timeout;
|
public uint Timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parameters for use with SystemParametersInfo.
|
||||||
|
/// </summary>
|
||||||
|
public enum AccessibilityParameter
|
||||||
|
{
|
||||||
|
#pragma warning disable SA1602
|
||||||
|
SPI_GETCLIENTAREAANIMATION = 0x1042,
|
||||||
|
#pragma warning restore SA1602
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves or sets the value of one of the system-wide parameters. This function can also update the user profile while setting a parameter.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uiAction">The system-wide parameter to be retrieved or set.</param>
|
||||||
|
/// <param name="uiParam">A parameter whose usage and format depends on the system parameter being queried or set.</param>
|
||||||
|
/// <param name="pvParam">A parameter whose usage and format depends on the system parameter being queried or set. If not otherwise indicated, you must specify zero for this parameter.</param>
|
||||||
|
/// <param name="fWinIni">If a system parameter is being set, specifies whether the user profile is to be updated, and if so, whether the WM_SETTINGCHANGE message is to be broadcast to all top-level windows to notify them of the change.</param>
|
||||||
|
/// <returns>If the function succeeds, the return value is a nonzero value.</returns>
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
public static extern bool SystemParametersInfo(uint uiAction, uint uiParam, ref int pvParam, uint fWinIni);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,8 @@ internal partial class PluginManager : IInternalDisposableService
|
||||||
this.configuration.PluginTestingOptIns ??= new();
|
this.configuration.PluginTestingOptIns ??= new();
|
||||||
this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient);
|
this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient);
|
||||||
|
|
||||||
this.ApplyPatches();
|
// NET8 CHORE
|
||||||
|
// this.ApplyPatches();
|
||||||
|
|
||||||
registerStartupBlocker(
|
registerStartupBlocker(
|
||||||
Task.Run(this.LoadAndStartLoadSyncPlugins),
|
Task.Run(this.LoadAndStartLoadSyncPlugins),
|
||||||
|
|
@ -422,8 +423,9 @@ internal partial class PluginManager : IInternalDisposableService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.assemblyLocationMonoHook?.Dispose();
|
// NET8 CHORE
|
||||||
this.assemblyCodeBaseMonoHook?.Dispose();
|
// this.assemblyLocationMonoHook?.Dispose();
|
||||||
|
// this.assemblyCodeBaseMonoHook?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -842,7 +844,8 @@ internal partial class PluginManager : IInternalDisposableService
|
||||||
this.installedPluginsList.Remove(plugin);
|
this.installedPluginsList.Remove(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
// NET8 CHORE
|
||||||
|
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||||
|
|
||||||
this.NotifyinstalledPluginsListChanged();
|
this.NotifyinstalledPluginsListChanged();
|
||||||
this.NotifyAvailablePluginsChanged();
|
this.NotifyAvailablePluginsChanged();
|
||||||
|
|
@ -1583,7 +1586,8 @@ internal partial class PluginManager : IInternalDisposableService
|
||||||
}
|
}
|
||||||
catch (InvalidPluginException)
|
catch (InvalidPluginException)
|
||||||
{
|
{
|
||||||
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
// NET8 CHORE
|
||||||
|
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (BannedPluginException)
|
catch (BannedPluginException)
|
||||||
|
|
@ -1629,7 +1633,8 @@ internal partial class PluginManager : IInternalDisposableService
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
// NET8 CHORE
|
||||||
|
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1756,6 +1761,8 @@ internal partial class PluginManager : IInternalDisposableService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NET8 CHORE
|
||||||
|
/*
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class responsible for loading and unloading plugins.
|
/// Class responsible for loading and unloading plugins.
|
||||||
/// This contains the assembly patching functionality to resolve assembly locations.
|
/// This contains the assembly patching functionality to resolve assembly locations.
|
||||||
|
|
@ -1863,3 +1870,4 @@ internal partial class PluginManager
|
||||||
this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch);
|
this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -404,7 +404,8 @@ internal class LocalPlugin : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the location for the Location and CodeBase patches
|
// Update the location for the Location and CodeBase patches
|
||||||
PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
|
// NET8 CHORE
|
||||||
|
// PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
|
||||||
|
|
||||||
this.DalamudInterface =
|
this.DalamudInterface =
|
||||||
new DalamudPluginInterface(this, reason);
|
new DalamudPluginInterface(this, reason);
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,27 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
|
||||||
namespace Dalamud.Plugin.Services;
|
namespace Dalamud.Plugin.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This collection represents the currently spawned FFXIV game objects.
|
/// This collection represents the currently spawned FFXIV game objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Api10ToDo(
|
||||||
|
"Make it an IEnumerable<GameObject> instead. Skipping null objects make IReadOnlyCollection<T>.Count yield incorrect values.")]
|
||||||
public interface IObjectTable : IReadOnlyCollection<GameObject>
|
public interface IObjectTable : IReadOnlyCollection<GameObject>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the address of the object table.
|
/// Gets the address of the object table.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public nint Address { get; }
|
public nint Address { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the length of the object table.
|
/// Gets the length of the object table.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Length { get; }
|
public int Length { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get an object at the specified spawn index.
|
/// Get an object at the specified spawn index.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -32,14 +35,14 @@ public interface IObjectTable : IReadOnlyCollection<GameObject>
|
||||||
/// <param name="objectId">Object ID to find.</param>
|
/// <param name="objectId">Object ID to find.</param>
|
||||||
/// <returns>A game object or null.</returns>
|
/// <returns>A game object or null.</returns>
|
||||||
public GameObject? SearchById(ulong objectId);
|
public GameObject? SearchById(ulong objectId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the address of the game object at the specified index of the object table.
|
/// Gets the address of the game object at the specified index of the object table.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="index">The index of the object.</param>
|
/// <param name="index">The index of the object.</param>
|
||||||
/// <returns>The memory address of the object.</returns>
|
/// <returns>The memory address of the object.</returns>
|
||||||
public nint GetObjectAddress(int index);
|
public nint GetObjectAddress(int index);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a reference to an FFXIV game object.
|
/// Create a reference to an FFXIV game object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,9 @@ public static class DateTimeSpanExtensions
|
||||||
|
|
||||||
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
|
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
|
||||||
/// <param name="when">When.</param>
|
/// <param name="when">When.</param>
|
||||||
|
/// <param name="floorBy">The alignment unit of time span.</param>
|
||||||
/// <returns>The formatted string.</returns>
|
/// <returns>The formatted string.</returns>
|
||||||
public static string LocRelativePastLong(this DateTime when)
|
public static string LocRelativePastLong(this DateTime when, TimeSpan floorBy)
|
||||||
{
|
{
|
||||||
var loc = Loc.Localize(
|
var loc = Loc.Localize(
|
||||||
"DateTimeSpanExtensions.RelativeFormatStringsLong",
|
"DateTimeSpanExtensions.RelativeFormatStringsLong",
|
||||||
|
|
@ -55,13 +56,17 @@ public static class DateTimeSpanExtensions
|
||||||
if (relativeFormatStringLong?.FormatStringLoc != loc)
|
if (relativeFormatStringLong?.FormatStringLoc != loc)
|
||||||
relativeFormatStringLong ??= new(loc);
|
relativeFormatStringLong ??= new(loc);
|
||||||
|
|
||||||
return relativeFormatStringLong.Format(DateTime.Now - when);
|
return
|
||||||
|
floorBy == default
|
||||||
|
? relativeFormatStringLong.Format(DateTime.Now - when)
|
||||||
|
: relativeFormatStringLong.Format(Math.Floor((DateTime.Now - when) / floorBy) * floorBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
|
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
|
||||||
/// <param name="when">When.</param>
|
/// <param name="when">When.</param>
|
||||||
|
/// <param name="floorBy">The alignment unit of time span.</param>
|
||||||
/// <returns>The formatted string.</returns>
|
/// <returns>The formatted string.</returns>
|
||||||
public static string LocRelativePastShort(this DateTime when)
|
public static string LocRelativePastShort(this DateTime when, TimeSpan floorBy)
|
||||||
{
|
{
|
||||||
var loc = Loc.Localize(
|
var loc = Loc.Localize(
|
||||||
"DateTimeSpanExtensions.RelativeFormatStringsShort",
|
"DateTimeSpanExtensions.RelativeFormatStringsShort",
|
||||||
|
|
@ -71,9 +76,22 @@ public static class DateTimeSpanExtensions
|
||||||
if (relativeFormatStringShort?.FormatStringLoc != loc)
|
if (relativeFormatStringShort?.FormatStringLoc != loc)
|
||||||
relativeFormatStringShort = new(loc);
|
relativeFormatStringShort = new(loc);
|
||||||
|
|
||||||
return relativeFormatStringShort.Format(DateTime.Now - when);
|
return
|
||||||
|
floorBy == default
|
||||||
|
? relativeFormatStringShort.Format(DateTime.Now - when)
|
||||||
|
: relativeFormatStringShort.Format(Math.Floor((DateTime.Now - when) / floorBy) * floorBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
|
||||||
|
/// <param name="when">When.</param>
|
||||||
|
/// <returns>The formatted string.</returns>
|
||||||
|
public static string LocRelativePastLong(this DateTime when) => when.LocRelativePastLong(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
|
||||||
|
/// <param name="when">When.</param>
|
||||||
|
/// <returns>The formatted string.</returns>
|
||||||
|
public static string LocRelativePastShort(this DateTime when) => when.LocRelativePastShort(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
private sealed class ParsedRelativeFormatStrings
|
private sealed class ParsedRelativeFormatStrings
|
||||||
{
|
{
|
||||||
private readonly List<(float MinSeconds, string FormatString)> formatStrings = new();
|
private readonly List<(float MinSeconds, string FormatString)> formatStrings = new();
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,9 @@ namespace Dalamud.Utility
|
||||||
this.firstIndex = 0;
|
this.firstIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else // value < this._size
|
else
|
||||||
{
|
{
|
||||||
|
// value < this._size
|
||||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(value), value, 0);
|
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(value), value, 0);
|
||||||
if (value < this.Count)
|
if (value < this.Count)
|
||||||
{
|
{
|
||||||
|
|
@ -156,14 +157,14 @@ namespace Dalamud.Utility
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Add items to this <see cref="RollingList{T}"/>.</summary>
|
/// <summary>Add items to this <see cref="RollingList{T}"/>.</summary>
|
||||||
/// <param name="items">Items to add.</param>
|
/// <param name="range">Items to add.</param>
|
||||||
public void AddRange(IEnumerable<T> items)
|
public void AddRange(IEnumerable<T> range)
|
||||||
{
|
{
|
||||||
if (this.size == 0) return;
|
if (this.size == 0) return;
|
||||||
foreach (var item in items) this.Add(item);
|
foreach (var item in range) this.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Removes all elements from the <see cref="RollingList{T}"/></summary>
|
/// <summary>Removes all elements from the <see cref="RollingList{T}"/>.</summary>
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
this.items.Clear();
|
this.items.Clear();
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ internal static class SignatureHelper
|
||||||
|
|
||||||
switch (sig.UseFlags)
|
switch (sig.UseFlags)
|
||||||
{
|
{
|
||||||
case SignatureUseFlags.Auto when actualType == typeof(IntPtr) || actualType.IsPointer || actualType.IsAssignableTo(typeof(Delegate)):
|
case SignatureUseFlags.Auto when actualType == typeof(IntPtr) || actualType.IsFunctionPointer || actualType.IsUnmanagedFunctionPointer || actualType.IsPointer || actualType.IsAssignableTo(typeof(Delegate)):
|
||||||
case SignatureUseFlags.Pointer:
|
case SignatureUseFlags.Pointer:
|
||||||
{
|
{
|
||||||
if (actualType.IsAssignableTo(typeof(Delegate)))
|
if (actualType.IsAssignableTo(typeof(Delegate)))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "7.0.0",
|
"version": "8.0.0",
|
||||||
"rollForward": "latestMajor",
|
"rollForward": "latestMinor",
|
||||||
"allowPrerelease": true
|
"allowPrerelease": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit ac2ced26fc98153c65f5b8f0eaf0f464258ff683
|
Subproject commit 2c885a35e0edf8ab7a335e3296f06642837afbec
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<Platforms>x64</Platforms>
|
<Platforms>x64</Platforms>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue