Compare commits
39 Commits
4495177f02
...
2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
d777dc599f
|
|||
|
e3d9300ca3
|
|||
|
699678f641
|
|||
|
7a391e6253
|
|||
|
620ebf9195
|
|||
|
8cc4f34c55
|
|||
|
513845b811
|
|||
|
84586cac3d
|
|||
|
b4108c7803
|
|||
|
d891dceb28
|
|||
|
89fa1a999f
|
|||
|
1f6e86ec2d
|
|||
|
d225a3844a
|
|||
|
d4a46910f9
|
|||
|
b59a579f56
|
|||
|
7706ef1fa7
|
|||
|
fca730557e
|
|||
|
6572fdcc27
|
|||
|
bf770f19d9
|
|||
|
78089a9fc7
|
|||
|
3c81e1f243
|
|||
|
0808266887
|
|||
|
a2071b9c05
|
|||
|
612e7c88a2
|
|||
|
1755b5cb54
|
|||
|
4a388dcfa9
|
|||
|
a0957715a5
|
|||
|
04a8ee3186
|
|||
|
b79a51748f
|
|||
|
95d9f65068
|
|||
|
a70968d30c
|
|||
|
6ebb73040b
|
|||
|
46f2443824
|
|||
|
eeab8354b6
|
|||
|
b5d8f288f9
|
|||
| 3c2dab4d21 | |||
| edb49f710a | |||
| 9ff21dc341 | |||
| 17962a37b3 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -2,15 +2,20 @@
|
|||||||
## files generated by popular Visual Studio add-ons.
|
## files generated by popular Visual Studio add-ons.
|
||||||
##
|
##
|
||||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
.idea
|
||||||
|
qodana.yaml
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.rsuser
|
*.rsuser
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
*.userosscache
|
*.userosscache
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
*.bak
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
MareSynchronos/.DS_Store
|
||||||
|
*.zip
|
||||||
|
UmbraServer_extracted/
|
||||||
|
NuGet.config
|
||||||
|
Directory.Build.props
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
*.userprefs
|
*.userprefs
|
||||||
|
|||||||
23
.gitmodules
vendored
23
.gitmodules
vendored
@@ -1,15 +1,12 @@
|
|||||||
[submodule "UmbraAPI"]
|
[submodule "MareAPI"]
|
||||||
path = UmbraAPI
|
path = MareAPI
|
||||||
url = https://git.umbra-sync.net/SirConstance/UmbraAPI.git
|
url = ssh://git@git.umbra-sync.net:1222/Keda/UmbraAPI.git
|
||||||
|
branch = main
|
||||||
[submodule "Penumbra.Api"]
|
[submodule "Penumbra.Api"]
|
||||||
path = Penumbra.Api
|
path = Penumbra.Api
|
||||||
url = https://github.com/Ottermandias/Penumbra.Api.git
|
url = https://github.com/Ottermandias/Penumbra.Api.git
|
||||||
|
branch = main
|
||||||
[submodule "Glamourer.Api"]
|
[submodule "Glamourer.Api"]
|
||||||
path = Glamourer.Api
|
path = Glamourer.Api
|
||||||
url = https://github.com/Ottermandias/Glamourer.Api
|
url = https://github.com/Ottermandias/Glamourer.Api.git
|
||||||
|
branch = main
|
||||||
[submodule "UmbraCrypt"]
|
|
||||||
path = UmbraCrypt
|
|
||||||
url = https://git.umbra-sync.net/SirConstance/UmbraCrypt.git
|
|
||||||
|
|||||||
1
Glamourer.Api
Submodule
1
Glamourer.Api
Submodule
Submodule Glamourer.Api added at 59a7ab5fa9
File diff suppressed because it is too large
Load Diff
3
Glamourer.Api/.gitignore
vendored
3
Glamourer.Api/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
bin/
|
|
||||||
obj/
|
|
||||||
.vs/
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace Glamourer.Api.Api;
|
|
||||||
|
|
||||||
/// <summary> The full API available. </summary>
|
|
||||||
public interface IGlamourerApi : IGlamourerApiBase
|
|
||||||
{
|
|
||||||
/// <inheritdoc cref="IGlamourerApiDesigns"/>
|
|
||||||
public IGlamourerApiDesigns Designs { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems"/>
|
|
||||||
public IGlamourerApiItems Items { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState"/>
|
|
||||||
public IGlamourerApiState State { get; }
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
namespace Glamourer.Api.Api;
|
|
||||||
|
|
||||||
/// <summary> Basic API functions. </summary>
|
|
||||||
public interface IGlamourerApiBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get the current API version of the Glamourer available in this installation.
|
|
||||||
/// Major version changes indicate incompatibilities, minor version changes are backward-compatible additions.
|
|
||||||
/// </summary>
|
|
||||||
public (int Major, int Minor) ApiVersion { get; }
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using Glamourer.Api.Enums;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.Api;
|
|
||||||
|
|
||||||
/// <summary> All functions related to Glamourer designs. </summary>
|
|
||||||
public interface IGlamourerApiDesigns
|
|
||||||
{
|
|
||||||
/// <summary> Obtain a list of all available designs. </summary>
|
|
||||||
/// <returns> A dictionary of all designs from their GUID to their current display name. </returns>
|
|
||||||
public Dictionary<Guid, string> GetDesignList();
|
|
||||||
|
|
||||||
/// <summary> Apply an existing design to an actor. </summary>
|
|
||||||
/// <param name="designId"> The GUID of the design to apply. </param>
|
|
||||||
/// <param name="objectIndex"> The game object index of the actor to be manipulated. </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once, Equipment, Customization, Lock (see <see cref="ApplyFlag"/>.)</param>
|
|
||||||
/// <returns> DesignNotFound, ActorNotFound, InvalidKey, Success. </returns>
|
|
||||||
public GlamourerApiEc ApplyDesign(Guid designId, int objectIndex, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Apply an existing design to an actor. </summary>
|
|
||||||
/// <param name="designId"> The GUID of the design to apply. </param>
|
|
||||||
/// <param name="playerName"> The name of the players to be manipulated. </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once, Equipment, Customization, Lock (see <see cref="ApplyFlag"/>.)</param>
|
|
||||||
/// <returns> DesignNotFound, ActorNotFound, InvalidKey, Success. </returns>
|
|
||||||
/// /// <remarks>
|
|
||||||
/// The player does not have to be currently available as long as he has a persisted Glamourer state.<br/>
|
|
||||||
/// Only players are checked for name equality, no NPCs.<br/>
|
|
||||||
/// If multiple players of the same name are found, all of them are reverted.<br/>
|
|
||||||
/// Prefer to use the index-based function unless you need to get the state of someone currently unavailable.
|
|
||||||
/// </remarks>
|
|
||||||
public GlamourerApiEc ApplyDesignName(Guid designId, string playerName, uint key, ApplyFlag flags);
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
using Glamourer.Api.Enums;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.Api;
|
|
||||||
|
|
||||||
/// <summary> All functions related to items. </summary>
|
|
||||||
public interface IGlamourerApiItems
|
|
||||||
{
|
|
||||||
/// <summary> Set a single item on an actor. </summary>
|
|
||||||
/// <param name="objectIndex"> The game object index of the actor to be manipulated. </param>
|
|
||||||
/// <param name="slot"> The slot to apply the item to. </param>
|
|
||||||
/// <param name="itemId"> The (Custom) ID of the item to apply. </param>
|
|
||||||
/// <param name="stains"> The IDs of the stains to apply to the item. </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once (see <see cref="ApplyFlag"/>.)</param>
|
|
||||||
/// <returns> ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. </returns>
|
|
||||||
/// <remarks> The item ID can be a custom item ID in Glamourer's format for models without an associated item, or a normal game item ID. </remarks>
|
|
||||||
public GlamourerApiEc SetItem(int objectIndex, ApiEquipSlot slot, ulong itemId, IReadOnlyList<byte> stains, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Set a single item on players. </summary>
|
|
||||||
/// <param name="playerName"> The name of the players to be manipulated. </param>
|
|
||||||
/// <param name="slot"> The slot to apply the item to. </param>
|
|
||||||
/// <param name="itemId"> The (Custom) ID of the item to apply. </param>
|
|
||||||
/// <param name="stains"> The IDs of the stains to apply to the item. </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once (see <see cref="ApplyFlag"/>.)</param>
|
|
||||||
/// <returns> ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// The item ID can be a custom item ID in Glamourer's format for models without an associated item, or a normal game item ID.<br/>
|
|
||||||
/// The player does not have to be currently available as long as he has a persisted Glamourer state.<br/>
|
|
||||||
/// Only players are checked for name equality, no NPCs.<br/>
|
|
||||||
/// If multiple players of the same name are found, all of them are modified.<br/>
|
|
||||||
/// Prefer to use the index-based function unless you need to get the state of someone currently unavailable.
|
|
||||||
/// </remarks>
|
|
||||||
public GlamourerApiEc SetItemName(string playerName, ApiEquipSlot slot, ulong itemId, IReadOnlyList<byte> stains, uint key,
|
|
||||||
ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Set a single bonus item on an actor. </summary>
|
|
||||||
/// <param name="objectIndex"> The game object index of the actor to be manipulated. </param>
|
|
||||||
/// <param name="slot"> The bonus slot to apply the item to. </param>
|
|
||||||
/// <param name="bonusItemId"> The bonus item sheet ID of the item to apply (including stain). </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once (see <see cref="ApplyFlag"/>.)</param>
|
|
||||||
/// <returns> ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. </returns>
|
|
||||||
/// <remarks> The bonus item ID can currently not be a custom item ID in Glamourer's format for models without an associated item. Use 0 to remove the bonus item. </remarks>
|
|
||||||
public GlamourerApiEc SetBonusItem(int objectIndex, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Set a single bonus item on an actor. </summary>
|
|
||||||
/// <param name="playerName"> The game object index of the actor to be manipulated. </param>
|
|
||||||
/// <param name="slot"> The bonus slot to apply the item to. </param>
|
|
||||||
/// <param name="bonusItemId"> The bonus item sheet ID of the item to apply (including stain). </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once (see <see cref="ApplyFlag"/>.)</param>
|
|
||||||
/// <returns> ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// The bonus item ID can currently not be a custom item ID in Glamourer's format for models without an associated item. Use 0 to remove the bonus item. <br/>
|
|
||||||
/// The player does not have to be currently available as long as he has a persisted Glamourer state.<br/>
|
|
||||||
/// Only players are checked for name equality, no NPCs.<br/>
|
|
||||||
/// If multiple players of the same name are found, all of them are modified.<br/>
|
|
||||||
/// Prefer to use the index-based function unless you need to get the state of someone currently unavailable.
|
|
||||||
/// </remarks>
|
|
||||||
public GlamourerApiEc SetBonusItemName(string playerName, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Set the defined Meta State flags to the active or inactive state on actor. </summary>
|
|
||||||
/// <param name="objectIndex"> The game object index of the actor to be manipulated. </param>
|
|
||||||
/// <param name="types"> The flags defining which meta states to update to the new value. This can be multiple at once. </param>
|
|
||||||
/// <param name="newValue"> The new value to update to. </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once (see <see cref="ApplyFlag.Once"/>.)</param>
|
|
||||||
/// <returns> ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. </returns>
|
|
||||||
public GlamourerApiEc SetMetaState(int objectIndex, MetaFlag types, bool newValue, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Set the defined Meta State flags to the active or inactive state on actor (by name) </summary>
|
|
||||||
/// <param name="playerName"> The name of the players to be manipulated. </param>
|
|
||||||
/// <param name="types"> The flags defining which meta states to update to the new value. This can be multiple at once. </param>
|
|
||||||
/// <param name="newValue"> The new value to update to. </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once (see <see cref="ApplyFlag.Once"/>.)</param>
|
|
||||||
/// <returns> ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. </returns>
|
|
||||||
public GlamourerApiEc SetMetaStateName(string playerName, MetaFlag types, bool newValue, uint key, ApplyFlag flags);
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
using Glamourer.Api.Enums;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.Api;
|
|
||||||
|
|
||||||
/// <summary> Any functions related to Glamourer's state tracking. </summary>
|
|
||||||
public interface IGlamourerApiState
|
|
||||||
{
|
|
||||||
/// <summary> Get the current Glamourer state of an actor. </summary>
|
|
||||||
/// <param name="objectIndex"> The game object index of the desired actor. </param>
|
|
||||||
/// <param name="key"> A key to unlock the state if necessary. </param>
|
|
||||||
/// <returns> ActorNotFound, InvalidKey or Success, and the state on success. </returns>
|
|
||||||
/// <remarks> The actor does not need to have a prior Glamourer state as long as it can be found. </remarks>
|
|
||||||
public (GlamourerApiEc, JObject?) GetState(int objectIndex, uint key);
|
|
||||||
|
|
||||||
/// <summary> Get the current Glamourer state of a player character. </summary>
|
|
||||||
/// <param name="playerName"> The name of the desired player. </param>
|
|
||||||
/// <param name="key"> A key to unlock the state if necessary. </param>
|
|
||||||
/// <returns> ActorNotFound, InvalidKey or Success, and the state on success. </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// The player does not have to be currently available as long as he has a persisted Glamourer state.
|
|
||||||
/// Only players are checked for name equality, no NPCs.
|
|
||||||
/// If multiple players of the same name are found, the first is returned.
|
|
||||||
/// Prefer to use the index-based function unless you need to get the state of someone currently unavailable.
|
|
||||||
/// </remarks>
|
|
||||||
public (GlamourerApiEc, JObject?) GetStateName(string playerName, uint key);
|
|
||||||
|
|
||||||
/// <inheritdoc cref="GetState"/>
|
|
||||||
public (GlamourerApiEc, string?) GetStateBase64(int objectIndex, uint key);
|
|
||||||
|
|
||||||
/// <inheritdoc cref="GetStateName"/>
|
|
||||||
public (GlamourerApiEc, string?) GetStateBase64Name(string objectName, uint key);
|
|
||||||
|
|
||||||
/// <summary> Apply a supplied state to an actor. </summary>
|
|
||||||
/// <param name="applyState"> The state, which can be either a Glamourer-supplied JObject or a Base64 string. </param>
|
|
||||||
/// <param name="objectIndex"> The game object index of the actor to be manipulated. </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the application. Respects Once, Equipment, Customization and Lock (see <see cref="ApplyFlag"/>.) </param>
|
|
||||||
/// <returns> ActorNotFound, InvalidKey, ActorNotHuman, Success. </returns>
|
|
||||||
public GlamourerApiEc ApplyState(object applyState, int objectIndex, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Apply a supplied state to players. </summary>
|
|
||||||
/// <param name="applyState"> The state, which can be either a Glamourer-supplied JObject or a Base64 string. </param>
|
|
||||||
/// <param name="playerName"> The name of the player to be manipulated. </param>
|
|
||||||
/// <param name="key"> A key to unlock or lock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the application. Respects Once, Equipment, Customization and Lock (see <see cref="ApplyFlag"/>.) </param>
|
|
||||||
/// <returns> ActorNotFound, InvalidKey, ActorNotHuman, Success. </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// The player does not have to be currently available as long as he has a persisted Glamourer state.<br/>
|
|
||||||
/// Only players are checked for name equality, no NPCs.<br/>
|
|
||||||
/// If multiple players of the same name are found, all of them are manipulated.<br/>
|
|
||||||
/// Prefer to use the index-based function unless you need to get the state of someone currently unavailable.
|
|
||||||
/// </remarks>
|
|
||||||
public GlamourerApiEc ApplyStateName(object applyState, string playerName, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Revert the Glamourer state of an actor to Game state. </summary>
|
|
||||||
/// <param name="objectIndex"> The game object index of the actor to be manipulated. </param>
|
|
||||||
/// <param name="key"> A key to unlock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Equipment and Customization (see <see cref="ApplyFlag"/>.) </param>
|
|
||||||
/// <returns> ActorNotFound, InvalidKey, Success, NothingDone. </returns>
|
|
||||||
public GlamourerApiEc RevertState(int objectIndex, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Revert the Glamourer state of players to game state. </summary>
|
|
||||||
/// <param name="playerName"> The name of the players to be reverted. </param>
|
|
||||||
/// <param name="key"> A key to unlock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Equipment and Customization (see <see cref="ApplyFlag"/>.) </param>
|
|
||||||
/// <returns> ActorNotFound, InvalidKey, Success, NothingDone. </returns>
|
|
||||||
/// /// <remarks>
|
|
||||||
/// The player does not have to be currently available as long as he has a persisted Glamourer state.<br/>
|
|
||||||
/// Only players are checked for name equality, no NPCs.<br/>
|
|
||||||
/// If multiple players of the same name are found, all of them are reverted.<br/>
|
|
||||||
/// Prefer to use the index-based function unless you need to get the state of someone currently unavailable.
|
|
||||||
/// </remarks>
|
|
||||||
public GlamourerApiEc RevertStateName(string playerName, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Unlock the Glamourer state of an actor with a key. </summary>
|
|
||||||
/// <param name="objectIndex"> The game object index of the actor to be manipulated. </param>
|
|
||||||
/// <param name="key"> A key to unlock the state. </param>
|
|
||||||
/// <returns> ActorNotFound, InvalidKey, Success, NothingDone. </returns>
|
|
||||||
public GlamourerApiEc UnlockState(int objectIndex, uint key);
|
|
||||||
|
|
||||||
/// <summary> Unlock the Glamourer state of players with a key. </summary>
|
|
||||||
/// <param name="playerName"> The name of the players to be unlocked. </param>
|
|
||||||
/// <param name="key"> A key to unlock the state. </param>
|
|
||||||
/// <returns> InvalidKey, Success, NothingDone. </returns>
|
|
||||||
public GlamourerApiEc UnlockStateName(string playerName, uint key);
|
|
||||||
|
|
||||||
/// <summary> Unlock all active glamourer states with a key. </summary>
|
|
||||||
/// <param name="key"> The key to unlock states with. </param>
|
|
||||||
/// <returns> The number of unlocked states. </returns>
|
|
||||||
public int UnlockAll(uint key);
|
|
||||||
|
|
||||||
/// <summary> Revert the Glamourer state of an actor to automation state. </summary>
|
|
||||||
/// <param name="objectIndex"> The game object index of the actor to be manipulated. </param>
|
|
||||||
/// <param name="key"> A key to unlock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once and Lock (see <see cref="ApplyFlag"/>.) </param>
|
|
||||||
/// <returns> ActorNotFound, InvalidKey, Success, NothingDone. </returns>
|
|
||||||
public GlamourerApiEc RevertToAutomation(int objectIndex, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Revert the Glamourer state of players to automation state. </summary>
|
|
||||||
/// <param name="playerName"> The name of the players to be reverted. </param>
|
|
||||||
/// <param name="key"> A key to unlock the state if necessary. </param>
|
|
||||||
/// <param name="flags"> The flags used for the reversion. Respects Once and Lock (see <see cref="ApplyFlag"/>.) </param>
|
|
||||||
/// <returns> ActorNotFound, InvalidKey, Success, NothingDone. </returns>
|
|
||||||
/// /// <remarks>
|
|
||||||
/// The player does not have to be currently available as long as he has a persisted Glamourer state.<br/>
|
|
||||||
/// Only players are checked for name equality, no NPCs.<br/>
|
|
||||||
/// If multiple players of the same name are found, all of them are reverted.<br/>
|
|
||||||
/// Prefer to use the index-based function unless you need to get the state of someone currently unavailable.
|
|
||||||
/// </remarks>
|
|
||||||
public GlamourerApiEc RevertToAutomationName(string playerName, uint key, ApplyFlag flags);
|
|
||||||
|
|
||||||
/// <summary> Invoked with the game object pointer (if available) whenever an actors tracked state changes. </summary>
|
|
||||||
public event Action<nint> StateChanged;
|
|
||||||
|
|
||||||
/// <summary> Invoked with the game object pointer (if available) whenever an actors tracked state changes, with the type of change. </summary>
|
|
||||||
public event Action<nint, StateChangeType> StateChangedWithType;
|
|
||||||
|
|
||||||
/// <summary> Invoked with the game object pointer (if available) whenever an actors tracked state finalizes a grouped change consisting of multiple smaller changes. </summary>
|
|
||||||
public event Action<nint, StateFinalizationType> StateFinalized;
|
|
||||||
|
|
||||||
/// <summary> Invoked when the player enters or leaves GPose (true => entered GPose, false => left GPose). </summary>
|
|
||||||
public event Action<bool>? GPoseChanged;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
namespace Glamourer.Api.Enums;
|
|
||||||
|
|
||||||
/// <summary> Bonus item slots restricted to API-relevant slots. </summary>
|
|
||||||
public enum ApiBonusSlot : byte
|
|
||||||
{
|
|
||||||
/// <summary> No slot. </summary>
|
|
||||||
Unknown = 0,
|
|
||||||
|
|
||||||
/// <summary> The Glasses slot. </summary>
|
|
||||||
Glasses = 1,
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
namespace Glamourer.Api.Enums;
|
|
||||||
|
|
||||||
/// <summary> Equip slots restricted to API-relevant slots, but compatible with GameData.EquipSlots. </summary>
|
|
||||||
public enum ApiEquipSlot : byte
|
|
||||||
{
|
|
||||||
/// <summary> No slot. </summary>
|
|
||||||
Unknown = 0,
|
|
||||||
|
|
||||||
/// <summary> Mainhand, also used for both-handed weapons. </summary>
|
|
||||||
MainHand = 1,
|
|
||||||
|
|
||||||
/// <summary> Offhand, used for shields or if you want to apply the offhand component of certain weapons. </summary>
|
|
||||||
OffHand = 2,
|
|
||||||
|
|
||||||
/// <summary> Head. </summary>
|
|
||||||
Head = 3,
|
|
||||||
|
|
||||||
/// <summary> Body. </summary>
|
|
||||||
Body = 4,
|
|
||||||
|
|
||||||
/// <summary> Hands. </summary>
|
|
||||||
Hands = 5,
|
|
||||||
|
|
||||||
/// <summary> Legs. </summary>
|
|
||||||
Legs = 7,
|
|
||||||
|
|
||||||
/// <summary> Feet. </summary>
|
|
||||||
Feet = 8,
|
|
||||||
|
|
||||||
/// <summary> Ears. </summary>
|
|
||||||
Ears = 9,
|
|
||||||
|
|
||||||
/// <summary> Neck. </summary>
|
|
||||||
Neck = 10,
|
|
||||||
|
|
||||||
/// <summary> Wrists. </summary>
|
|
||||||
Wrists = 11,
|
|
||||||
|
|
||||||
/// <summary> Right Finger. </summary>
|
|
||||||
RFinger = 12,
|
|
||||||
|
|
||||||
/// <summary> Left Finger. </summary>
|
|
||||||
/// <remarks> Not officially existing, means "weapon could be equipped in either hand" for the game. </remarks>
|
|
||||||
LFinger = 14,
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
namespace Glamourer.Api.Enums;
|
|
||||||
|
|
||||||
/// <summary> Application flags that can be used in different situations. </summary>
|
|
||||||
[Flags]
|
|
||||||
public enum ApplyFlag : ulong
|
|
||||||
{
|
|
||||||
/// <summary> Apply the selected manipulation only once, without forcing the state into automation. </summary>
|
|
||||||
Once = 0x01,
|
|
||||||
|
|
||||||
/// <summary> Apply the selected manipulation on the equipment (might be more or less supported). </summary>
|
|
||||||
Equipment = 0x02,
|
|
||||||
|
|
||||||
/// <summary> Apply the selected manipulation on the customizations (might be more or less supported). </summary>
|
|
||||||
Customization = 0x04,
|
|
||||||
|
|
||||||
/// <summary> Lock the state with the given key after applying the selected manipulation </summary>
|
|
||||||
Lock = 0x08,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Extensions for apply flags. </summary>
|
|
||||||
public static class ApplyFlagEx
|
|
||||||
{
|
|
||||||
/// <summary> The default application flags for design-based manipulations. </summary>
|
|
||||||
public const ApplyFlag DesignDefault = ApplyFlag.Once | ApplyFlag.Equipment | ApplyFlag.Customization;
|
|
||||||
|
|
||||||
/// <summary> The default application flags for state-based manipulations. </summary>
|
|
||||||
public const ApplyFlag StateDefault = ApplyFlag.Equipment | ApplyFlag.Customization | ApplyFlag.Lock;
|
|
||||||
|
|
||||||
/// <summary> The default application flags for reverse manipulations. </summary>
|
|
||||||
public const ApplyFlag RevertDefault = ApplyFlag.Equipment | ApplyFlag.Customization;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
namespace Glamourer.Api.Enums;
|
|
||||||
|
|
||||||
/// <summary> Return codes for API functions. </summary>
|
|
||||||
public enum GlamourerApiEc
|
|
||||||
{
|
|
||||||
/// <summary> The function succeeded. </summary>
|
|
||||||
Success = 0,
|
|
||||||
|
|
||||||
/// <summary> The function did not encounter a problem, but also did not do anything. </summary>
|
|
||||||
NothingDone = 1,
|
|
||||||
|
|
||||||
/// <summary> The requested actor was not found. </summary>
|
|
||||||
ActorNotFound = 2,
|
|
||||||
|
|
||||||
/// <summary> The requested actor was not human, but should have been. </summary>
|
|
||||||
ActorNotHuman,
|
|
||||||
|
|
||||||
/// <summary> The requested design was not found. </summary>
|
|
||||||
DesignNotFound,
|
|
||||||
|
|
||||||
/// <summary> The requested item was not found or could not be applied to the requested slot. </summary>
|
|
||||||
ItemInvalid,
|
|
||||||
|
|
||||||
/// <summary> The state of an actor could not be manipulated because it was locked and the provided key could not unlock it. </summary>
|
|
||||||
InvalidKey,
|
|
||||||
|
|
||||||
/// <summary> The provided object could not be converted into a valid Glamourer state to apply. </summary>
|
|
||||||
InvalidState,
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
namespace Glamourer.Api.Enums;
|
|
||||||
|
|
||||||
/// <summary> Application flags for setting the meta state of an actor. </summary>
|
|
||||||
[Flags]
|
|
||||||
public enum MetaFlag : ulong
|
|
||||||
{
|
|
||||||
Wetness = 0x01,
|
|
||||||
HatState = 0x02,
|
|
||||||
VisorState = 0x04,
|
|
||||||
WeaponState = 0x08,
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
namespace Glamourer.Api.Enums;
|
|
||||||
|
|
||||||
/// <summary> What type of information changed in a state. </summary>
|
|
||||||
public enum StateChangeType
|
|
||||||
{
|
|
||||||
/// <summary> A characters saved state had the model id changed. This means everything may have changed. </summary>
|
|
||||||
Model = 0,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had multiple customization values changed. </summary>
|
|
||||||
EntireCustomize = 1,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had a customization value changed. </summary>
|
|
||||||
Customize = 2,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had an equipment piece changed. </summary>
|
|
||||||
Equip = 3,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had its weapons changed. </summary>
|
|
||||||
Weapon = 4,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had a stain changed. </summary>
|
|
||||||
Stains = 5,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had a crest visibility changed. </summary>
|
|
||||||
Crest = 6,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had its customize parameter changed. </summary>
|
|
||||||
Parameter = 7,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had a material color table value changed. </summary>
|
|
||||||
MaterialValue = 8,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had a design applied. This means everything may have changed. </summary>
|
|
||||||
Design = 9,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had its state reset to its game values. </summary>
|
|
||||||
Reset = 10,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had a meta toggle changed. </summary>
|
|
||||||
Other = 11,
|
|
||||||
|
|
||||||
/// <summary> A characters state was reapplied. Data is null. </summary>
|
|
||||||
Reapply = 12,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had a bonus item changed. </summary>
|
|
||||||
BonusItem = 13,
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
namespace Glamourer.Api.Enums;
|
|
||||||
|
|
||||||
/// <summary> What type of Glamourer process was performed on the actors state to update it. </summary>
|
|
||||||
public enum StateFinalizationType
|
|
||||||
{
|
|
||||||
/// <summary> A characters saved state had the model id altered. </summary>
|
|
||||||
ModelChange = 0,
|
|
||||||
|
|
||||||
/// <summary> A singular Design was applied to an actors state. </summary>
|
|
||||||
DesignApplied = 1,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had been reset to game values. </summary>
|
|
||||||
Revert = 2,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had only its customization data reset to game state. </summary>
|
|
||||||
RevertCustomize = 3,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had only its equipment data reset to game state. </summary>
|
|
||||||
RevertEquipment = 4,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had its advanced values reverted to game state. </summary>
|
|
||||||
RevertAdvanced = 5,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state was reverted to automation state on top of their game state </summary>
|
|
||||||
RevertAutomation = 6,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had a generic reapply as a single operation. </summary>
|
|
||||||
Reapply = 7,
|
|
||||||
|
|
||||||
/// <summary> A characters saved state had their automation state reapplied over their existing state. </summary>
|
|
||||||
ReapplyAutomation = 8,
|
|
||||||
|
|
||||||
/// <summary> A characters save state finished applying all updated slots for game state on gearset change or initial load. </summary>
|
|
||||||
Gearset = 9,
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
|
|
||||||
<PropertyGroup>
|
|
||||||
<AssemblyTitle>Glamourer.Api</AssemblyTitle>
|
|
||||||
<Product>Glamourer</Product>
|
|
||||||
<Copyright>Copyright © 2025</Copyright>
|
|
||||||
<FileVersion>2.4.1.0</FileVersion>
|
|
||||||
<AssemblyVersion>2.4.1.0</AssemblyVersion>
|
|
||||||
<PackageVersion>2.4.1</PackageVersion>
|
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
|
||||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
<Title>Glamourer.Api</Title>
|
|
||||||
<Authors>Ottermandias</Authors>
|
|
||||||
<RepositoryUrl>https://github.com/Ottermandias/Glamourer</RepositoryUrl>
|
|
||||||
<Description>Auxiliary functions for Glamourers external API.</Description>
|
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<Use_DalamudPackager>false</Use_DalamudPackager>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ipc/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// Global using directives
|
|
||||||
|
|
||||||
global using System;
|
|
||||||
global using System.Collections.Generic;
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specialized subscriber only allowing to invoke actions.
|
|
||||||
/// </summary>
|
|
||||||
public class ActionSubscriber
|
|
||||||
{
|
|
||||||
private readonly ICallGateSubscriber<object?>? _subscriber;
|
|
||||||
|
|
||||||
/// <summary> Whether the subscriber could successfully be created. </summary>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
protected ActionSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<object?>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Invoke the action. See the source of the subscriber for details.</summary>
|
|
||||||
protected void Invoke()
|
|
||||||
=> _subscriber?.InvokeAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="ActionSubscriber"/>
|
|
||||||
public class ActionSubscriber<T1>
|
|
||||||
{
|
|
||||||
private readonly ICallGateSubscriber<T1, object?>? _subscriber;
|
|
||||||
|
|
||||||
/// <summary> Whether the subscriber could successfully be created. </summary>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
protected ActionSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, object?>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Invoke the action. See the source of the subscriber for details.</summary>
|
|
||||||
protected void Invoke(T1 a)
|
|
||||||
=> _subscriber?.InvokeAction(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="ActionSubscriber"/>
|
|
||||||
public class ActionSubscriber<T1, T2>
|
|
||||||
{
|
|
||||||
private readonly ICallGateSubscriber<T1, T2, object?>? _subscriber;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="ActionSubscriber{T1}.Valid"/>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
protected ActionSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, T2, object?>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="ActionSubscriber.Invoke"/>
|
|
||||||
protected void Invoke(T1 a, T2 b)
|
|
||||||
=> _subscriber?.InvokeAction(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="ActionSubscriber"/>
|
|
||||||
public class ActionSubscriber<T1, T2, T3>
|
|
||||||
{
|
|
||||||
private readonly ICallGateSubscriber<T1, T2, T3, object?>? _subscriber;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="ActionSubscriber{T1}.Valid"/>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
protected ActionSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, T2, T3, object?>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="ActionSubscriber.Invoke"/>
|
|
||||||
protected void Invoke(T1 a, T2 b, T3 c)
|
|
||||||
=> _subscriber?.InvokeAction(a, b, c);
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specialized disposable Provider for Events.<para />
|
|
||||||
/// Will execute the unsubscriber action on dispose if any is provided.<para />
|
|
||||||
/// Can only be invoked and disposed.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class EventProvider : IDisposable
|
|
||||||
{
|
|
||||||
private readonly IPluginLog _log;
|
|
||||||
private ICallGateProvider<object?>? _provider;
|
|
||||||
private Delegate? _unsubscriber;
|
|
||||||
|
|
||||||
public EventProvider(IDalamudPluginInterface pi, string label, (Action<Action> Add, Action<Action> Del)? subscribe = null)
|
|
||||||
{
|
|
||||||
_unsubscriber = null;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<object?>(label);
|
|
||||||
subscribe?.Add(Invoke);
|
|
||||||
_unsubscriber = subscribe?.Del;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public EventProvider(IDalamudPluginInterface pi, string label, Action<EventProvider> add, Action<EventProvider> del)
|
|
||||||
{
|
|
||||||
_unsubscriber = null;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<object?>(label);
|
|
||||||
add(this);
|
|
||||||
_unsubscriber = del;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Invoke the event.</summary>
|
|
||||||
public void Invoke()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider?.SendMessage();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Exception thrown on IPC event:\n{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
switch (_unsubscriber)
|
|
||||||
{
|
|
||||||
case Action<Action> a:
|
|
||||||
a(Invoke);
|
|
||||||
break;
|
|
||||||
case Action<EventProvider> b:
|
|
||||||
b(this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_unsubscriber = null;
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~EventProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="EventProvider"/>
|
|
||||||
public sealed class EventProvider<T1> : IDisposable
|
|
||||||
{
|
|
||||||
private readonly IPluginLog _log;
|
|
||||||
private ICallGateProvider<T1, object?>? _provider;
|
|
||||||
private Delegate? _unsubscriber;
|
|
||||||
|
|
||||||
public EventProvider(IDalamudPluginInterface pi, string label, (Action<Action<T1>> Add, Action<Action<T1>> Del)? subscribe = null)
|
|
||||||
{
|
|
||||||
_unsubscriber = null;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, object?>(label);
|
|
||||||
subscribe?.Add(Invoke);
|
|
||||||
_unsubscriber = subscribe?.Del;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public EventProvider(IDalamudPluginInterface pi, string label, Action<EventProvider<T1>> add, Action<EventProvider<T1>> del)
|
|
||||||
{
|
|
||||||
_unsubscriber = null;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, object?>(label);
|
|
||||||
add(this);
|
|
||||||
_unsubscriber = del;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="EventProvider.Invoke"/>
|
|
||||||
public void Invoke(T1 a)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider?.SendMessage(a);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Exception thrown on IPC event:\n{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
switch (_unsubscriber)
|
|
||||||
{
|
|
||||||
case Action<Action<T1>> a:
|
|
||||||
a(Invoke);
|
|
||||||
break;
|
|
||||||
case Action<EventProvider<T1>> b:
|
|
||||||
b(this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_unsubscriber = null;
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~EventProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="EventProvider"/>
|
|
||||||
public sealed class EventProvider<T1, T2> : IDisposable
|
|
||||||
{
|
|
||||||
private readonly IPluginLog _log;
|
|
||||||
private ICallGateProvider<T1, T2, object?>? _provider;
|
|
||||||
private Delegate? _unsubscriber;
|
|
||||||
|
|
||||||
public EventProvider(IDalamudPluginInterface pi, string label, (Action<Action<T1, T2>> Add, Action<Action<T1, T2>> Del)? subscribe = null)
|
|
||||||
{
|
|
||||||
_unsubscriber = null;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, T2, object?>(label);
|
|
||||||
subscribe?.Add(Invoke);
|
|
||||||
_unsubscriber = subscribe?.Del;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public EventProvider(IDalamudPluginInterface pi, string label, Action<EventProvider<T1, T2>> add, Action<EventProvider<T1, T2>> del)
|
|
||||||
{
|
|
||||||
_unsubscriber = null;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, T2, object?>(label);
|
|
||||||
add(this);
|
|
||||||
_unsubscriber = del;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="EventProvider.Invoke"/>
|
|
||||||
public void Invoke(T1 a, T2 b)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider?.SendMessage(a, b);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Exception thrown on IPC event:\n{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
switch (_unsubscriber)
|
|
||||||
{
|
|
||||||
case Action<Action<T1, T2>> a:
|
|
||||||
a(Invoke);
|
|
||||||
break;
|
|
||||||
case Action<EventProvider<T1, T2>> b:
|
|
||||||
b(this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_unsubscriber = null;
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~EventProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
@@ -1,394 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specialized disposable Subscriber for Events.<para />
|
|
||||||
/// Subscriptions are wrapped to be individually exception-safe.<para/>
|
|
||||||
/// Can be enabled and disabled.<para/>
|
|
||||||
/// </summary>
|
|
||||||
public sealed class EventSubscriber : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly IPluginLog _log;
|
|
||||||
private readonly Dictionary<Action, Action> _delegates = new();
|
|
||||||
private ICallGateSubscriber<object?>? _subscriber;
|
|
||||||
private bool _disabled;
|
|
||||||
|
|
||||||
public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<object?>(label);
|
|
||||||
foreach (var action in actions)
|
|
||||||
Event += action;
|
|
||||||
|
|
||||||
_disabled = false;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enable all currently subscribed actions registered with this EventSubscriber.
|
|
||||||
/// Does nothing if it is already enabled.
|
|
||||||
/// </summary>
|
|
||||||
public void Enable()
|
|
||||||
{
|
|
||||||
if (_disabled && _subscriber != null)
|
|
||||||
{
|
|
||||||
foreach (var action in _delegates.Values)
|
|
||||||
_subscriber.Subscribe(action);
|
|
||||||
|
|
||||||
_disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disable all subscribed actions registered with this EventSubscriber.
|
|
||||||
/// Does nothing if it is already disabled.
|
|
||||||
/// Does not forget the actions, only disables them.
|
|
||||||
/// </summary>
|
|
||||||
public void Disable()
|
|
||||||
{
|
|
||||||
if (!_disabled)
|
|
||||||
{
|
|
||||||
if (_subscriber != null)
|
|
||||||
foreach (var action in _delegates.Values)
|
|
||||||
_subscriber.Unsubscribe(action);
|
|
||||||
|
|
||||||
_disabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add or remove an action to the IPC event, if it is valid.
|
|
||||||
/// </summary>
|
|
||||||
public event Action Event
|
|
||||||
{
|
|
||||||
add
|
|
||||||
{
|
|
||||||
if (_subscriber != null && !_delegates.ContainsKey(value))
|
|
||||||
{
|
|
||||||
void Action()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
value();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Exception invoking IPC event {_label}:\n{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_delegates.TryAdd(value, Action) && !_disabled)
|
|
||||||
_subscriber.Subscribe(Action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remove
|
|
||||||
{
|
|
||||||
if (_subscriber != null && _delegates.Remove(value, out var action))
|
|
||||||
_subscriber.Unsubscribe(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Disable();
|
|
||||||
_subscriber = null;
|
|
||||||
_delegates.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
~EventSubscriber()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber"/> </summary>
|
|
||||||
public sealed class EventSubscriber<T1> : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly IPluginLog _log;
|
|
||||||
private readonly Dictionary<Action<T1>, Action<T1>> _delegates = new();
|
|
||||||
private ICallGateSubscriber<T1, object?>? _subscriber;
|
|
||||||
private bool _disabled;
|
|
||||||
|
|
||||||
public EventSubscriber(IDalamudPluginInterface pi, string label, params Action<T1>[] actions)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, object?>(label);
|
|
||||||
foreach (var action in actions)
|
|
||||||
Event += action;
|
|
||||||
|
|
||||||
_disabled = false;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber.Enable"/> </summary>
|
|
||||||
public void Enable()
|
|
||||||
{
|
|
||||||
if (_disabled && _subscriber != null)
|
|
||||||
{
|
|
||||||
foreach (var action in _delegates.Values)
|
|
||||||
_subscriber.Subscribe(action);
|
|
||||||
|
|
||||||
_disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber.Disable"/> </summary>
|
|
||||||
public void Disable()
|
|
||||||
{
|
|
||||||
if (!_disabled)
|
|
||||||
{
|
|
||||||
if (_subscriber != null)
|
|
||||||
foreach (var action in _delegates.Values)
|
|
||||||
_subscriber.Unsubscribe(action);
|
|
||||||
|
|
||||||
_disabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber.Event"/> </summary>
|
|
||||||
public event Action<T1> Event
|
|
||||||
{
|
|
||||||
add
|
|
||||||
{
|
|
||||||
if (_subscriber != null && !_delegates.ContainsKey(value))
|
|
||||||
{
|
|
||||||
void Action(T1 a)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
value(a);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Exception invoking IPC event {_label}:\n{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_delegates.TryAdd(value, Action) && !_disabled)
|
|
||||||
_subscriber.Subscribe(Action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remove
|
|
||||||
{
|
|
||||||
if (_subscriber != null && _delegates.Remove(value, out var action))
|
|
||||||
_subscriber.Unsubscribe(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Disable();
|
|
||||||
_subscriber = null;
|
|
||||||
_delegates.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
~EventSubscriber()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber"/> </summary>
|
|
||||||
public sealed class EventSubscriber<T1, T2> : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly IPluginLog _log;
|
|
||||||
private readonly Dictionary<Action<T1, T2>, Action<T1, T2>> _delegates = new();
|
|
||||||
private ICallGateSubscriber<T1, T2, object?>? _subscriber;
|
|
||||||
private bool _disabled;
|
|
||||||
|
|
||||||
public EventSubscriber(IDalamudPluginInterface pi, string label, params Action<T1, T2>[] actions)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, T2, object?>(label);
|
|
||||||
foreach (var action in actions)
|
|
||||||
Event += action;
|
|
||||||
|
|
||||||
_disabled = false;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber.Enable"/> </summary>
|
|
||||||
public void Enable()
|
|
||||||
{
|
|
||||||
if (_disabled && _subscriber != null)
|
|
||||||
{
|
|
||||||
foreach (var action in _delegates.Values)
|
|
||||||
_subscriber.Subscribe(action);
|
|
||||||
|
|
||||||
_disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber.Disable"/> </summary>
|
|
||||||
public void Disable()
|
|
||||||
{
|
|
||||||
if (!_disabled)
|
|
||||||
{
|
|
||||||
if (_subscriber != null)
|
|
||||||
foreach (var action in _delegates.Values)
|
|
||||||
_subscriber.Unsubscribe(action);
|
|
||||||
|
|
||||||
_disabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber.Event"/> </summary>
|
|
||||||
public event Action<T1, T2> Event
|
|
||||||
{
|
|
||||||
add
|
|
||||||
{
|
|
||||||
if (_subscriber != null && !_delegates.ContainsKey(value))
|
|
||||||
{
|
|
||||||
void Action(T1 a, T2 b)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
value(a, b);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Exception invoking IPC event {_label}:\n{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_delegates.TryAdd(value, Action) && !_disabled)
|
|
||||||
_subscriber.Subscribe(Action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remove
|
|
||||||
{
|
|
||||||
if (_subscriber != null && _delegates.Remove(value, out var action))
|
|
||||||
_subscriber.Unsubscribe(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Disable();
|
|
||||||
_subscriber = null;
|
|
||||||
_delegates.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
~EventSubscriber()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber"/> </summary>
|
|
||||||
public sealed class EventSubscriber<T1, T2, T3> : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly IPluginLog _log;
|
|
||||||
private readonly Dictionary<Action<T1, T2, T3>, Action<T1, T2, T3>> _delegates = new();
|
|
||||||
private ICallGateSubscriber<T1, T2, T3, object?>? _subscriber;
|
|
||||||
private bool _disabled;
|
|
||||||
|
|
||||||
public EventSubscriber(IDalamudPluginInterface pi, string label, params Action<T1, T2, T3>[] actions)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
_log = PluginLogHelper.GetLog(pi);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, T2, T3, object?>(label);
|
|
||||||
foreach (var action in actions)
|
|
||||||
Event += action;
|
|
||||||
|
|
||||||
_disabled = false;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber.Enable"/> </summary>
|
|
||||||
public void Enable()
|
|
||||||
{
|
|
||||||
if (_disabled && _subscriber != null)
|
|
||||||
{
|
|
||||||
foreach (var action in _delegates.Values)
|
|
||||||
_subscriber.Subscribe(action);
|
|
||||||
|
|
||||||
_disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber.Disable"/> </summary>
|
|
||||||
public void Disable()
|
|
||||||
{
|
|
||||||
if (!_disabled)
|
|
||||||
{
|
|
||||||
if (_subscriber != null)
|
|
||||||
foreach (var action in _delegates.Values)
|
|
||||||
_subscriber.Unsubscribe(action);
|
|
||||||
|
|
||||||
_disabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary><inheritdoc cref="EventSubscriber.Event"/> </summary>
|
|
||||||
public event Action<T1, T2, T3> Event
|
|
||||||
{
|
|
||||||
add
|
|
||||||
{
|
|
||||||
if (_subscriber != null && !_delegates.ContainsKey(value))
|
|
||||||
{
|
|
||||||
void Action(T1 a, T2 b, T3 c)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
value(a, b, c);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_log.Error($"Exception invoking IPC event {_label}:\n{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_delegates.TryAdd(value, Action) && !_disabled)
|
|
||||||
_subscriber.Subscribe(Action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remove
|
|
||||||
{
|
|
||||||
if (_subscriber != null && _delegates.Remove(value, out var action))
|
|
||||||
_subscriber.Unsubscribe(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Disable();
|
|
||||||
_subscriber = null;
|
|
||||||
_delegates.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
~EventSubscriber()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specialized disposable Provider for Funcs.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FuncProvider<TRet> : IDisposable
|
|
||||||
{
|
|
||||||
private ICallGateProvider<TRet>? _provider;
|
|
||||||
|
|
||||||
public FuncProvider(IDalamudPluginInterface pi, string label, Func<TRet> func)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_provider?.RegisterFunc(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_provider?.UnregisterFunc();
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~FuncProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncProvider{TRet}"/>
|
|
||||||
public sealed class FuncProvider<T1, TRet> : IDisposable
|
|
||||||
{
|
|
||||||
private ICallGateProvider<T1, TRet>? _provider;
|
|
||||||
|
|
||||||
public FuncProvider(IDalamudPluginInterface pi, string label, Func<T1, TRet> func)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_provider?.RegisterFunc(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_provider?.UnregisterFunc();
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~FuncProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncProvider{TRet}"/>
|
|
||||||
public sealed class FuncProvider<T1, T2, TRet> : IDisposable
|
|
||||||
{
|
|
||||||
private ICallGateProvider<T1, T2, TRet>? _provider;
|
|
||||||
|
|
||||||
public FuncProvider(IDalamudPluginInterface pi, string label, Func<T1, T2, TRet> func)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, T2, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_provider?.RegisterFunc(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_provider?.UnregisterFunc();
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~FuncProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncProvider{TRet}"/>
|
|
||||||
public sealed class FuncProvider<T1, T2, T3, TRet> : IDisposable
|
|
||||||
{
|
|
||||||
private ICallGateProvider<T1, T2, T3, TRet>? _provider;
|
|
||||||
|
|
||||||
public FuncProvider(IDalamudPluginInterface pi, string label, Func<T1, T2, T3, TRet> func)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, T2, T3, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_provider?.RegisterFunc(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_provider?.UnregisterFunc();
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~FuncProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncProvider{TRet}"/>
|
|
||||||
public sealed class FuncProvider<T1, T2, T3, T4, TRet> : IDisposable
|
|
||||||
{
|
|
||||||
private ICallGateProvider<T1, T2, T3, T4, TRet>? _provider;
|
|
||||||
|
|
||||||
public FuncProvider(IDalamudPluginInterface pi, string label, Func<T1, T2, T3, T4, TRet> func)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, T2, T3, T4, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_provider?.RegisterFunc(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_provider?.UnregisterFunc();
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~FuncProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncProvider{TRet}"/>
|
|
||||||
public sealed class FuncProvider<T1, T2, T3, T4, T5, TRet> : IDisposable
|
|
||||||
{
|
|
||||||
private ICallGateProvider<T1, T2, T3, T4, T5, TRet>? _provider;
|
|
||||||
|
|
||||||
public FuncProvider(IDalamudPluginInterface pi, string label, Func<T1, T2, T3, T4, T5, TRet> func)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, T2, T3, T4, T5, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_provider?.RegisterFunc(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_provider?.UnregisterFunc();
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~FuncProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncProvider{TRet}"/>
|
|
||||||
public sealed class FuncProvider<T1, T2, T3, T4, T5, T6, TRet> : IDisposable
|
|
||||||
{
|
|
||||||
private ICallGateProvider<T1, T2, T3, T4, T5, T6, TRet>? _provider;
|
|
||||||
|
|
||||||
public FuncProvider(IDalamudPluginInterface pi, string label, Func<T1, T2, T3, T4, T5, T6, TRet> func)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_provider = pi.GetIpcProvider<T1, T2, T3, T4, T5, T6, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}");
|
|
||||||
_provider = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_provider?.RegisterFunc(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_provider?.UnregisterFunc();
|
|
||||||
_provider = null;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
~FuncProvider()
|
|
||||||
=> Dispose();
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
using Dalamud.Plugin.Ipc.Exceptions;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specialized subscriber only allowing to invoke functions with a return.
|
|
||||||
/// </summary>
|
|
||||||
public class FuncSubscriber<TRet>
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly ICallGateSubscriber<TRet>? _subscriber;
|
|
||||||
|
|
||||||
/// <summary> Whether the subscriber could successfully be created. </summary>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
protected FuncSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Invoke the function. See the source of the subscriber for details.</summary>
|
|
||||||
protected TRet Invoke()
|
|
||||||
=> _subscriber != null ? _subscriber.InvokeFunc() : throw new IpcNotReadyError(_label);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
public class FuncSubscriber<T1, TRet>
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly ICallGateSubscriber<T1, TRet>? _subscriber;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Valid"/>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
protected FuncSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Invoke"/>
|
|
||||||
protected TRet Invoke(T1 a)
|
|
||||||
=> _subscriber != null ? _subscriber.InvokeFunc(a) : throw new IpcNotReadyError(_label);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
public class FuncSubscriber<T1, T2, TRet>
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly ICallGateSubscriber<T1, T2, TRet>? _subscriber;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Valid"/>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
protected FuncSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, T2, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Invoke"/>
|
|
||||||
protected TRet Invoke(T1 a, T2 b)
|
|
||||||
=> _subscriber != null ? _subscriber.InvokeFunc(a, b) : throw new IpcNotReadyError(_label);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
public class FuncSubscriber<T1, T2, T3, TRet>
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly ICallGateSubscriber<T1, T2, T3, TRet>? _subscriber;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Valid"/>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
protected FuncSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, T2, T3, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Invoke"/>
|
|
||||||
protected TRet Invoke(T1 a, T2 b, T3 c)
|
|
||||||
=> _subscriber != null ? _subscriber.InvokeFunc(a, b, c) : throw new IpcNotReadyError(_label);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
public class FuncSubscriber<T1, T2, T3, T4, TRet>
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly ICallGateSubscriber<T1, T2, T3, T4, TRet>? _subscriber;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Valid"/>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
protected FuncSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, T2, T3, T4, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Invoke"/>
|
|
||||||
protected TRet Invoke(T1 a, T2 b, T3 c, T4 d)
|
|
||||||
=> _subscriber != null ? _subscriber.InvokeFunc(a, b, c, d) : throw new IpcNotReadyError(_label);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
public class FuncSubscriber<T1, T2, T3, T4, T5, TRet>
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly ICallGateSubscriber<T1, T2, T3, T4, T5, TRet>? _subscriber;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Valid"/>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
protected FuncSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, T2, T3, T4, T5, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Invoke"/>
|
|
||||||
protected TRet Invoke(T1 a, T2 b, T3 c, T4 d, T5 e)
|
|
||||||
=> _subscriber != null ? _subscriber.InvokeFunc(a, b, c, d, e) : throw new IpcNotReadyError(_label);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
public class FuncSubscriber<T1, T2, T3, T4, T5, T6, TRet>
|
|
||||||
{
|
|
||||||
private readonly string _label;
|
|
||||||
private readonly ICallGateSubscriber<T1, T2, T3, T4, T5, T6, TRet>? _subscriber;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Valid"/>
|
|
||||||
public bool Valid
|
|
||||||
=> _subscriber != null;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}"/>
|
|
||||||
protected FuncSubscriber(IDalamudPluginInterface pi, string label)
|
|
||||||
{
|
|
||||||
_label = label;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_subscriber = pi.GetIpcSubscriber<T1, T2, T3, T4, T5, T6, TRet>(label);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}");
|
|
||||||
_subscriber = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FuncSubscriber{TRet}.Invoke"/>
|
|
||||||
protected TRet Invoke(T1 a, T2 b, T3 c, T4 d, T5 e, T6 f)
|
|
||||||
=> _subscriber != null ? _subscriber.InvokeFunc(a, b, c, d, e, f) : throw new IpcNotReadyError(_label);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using Dalamud.IoC;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
internal class PluginLogHelper
|
|
||||||
{
|
|
||||||
[PluginService]
|
|
||||||
private static IPluginLog? _log { get; set; }
|
|
||||||
|
|
||||||
private PluginLogHelper(IDalamudPluginInterface pi)
|
|
||||||
=> pi.Inject(this);
|
|
||||||
|
|
||||||
public static void WriteError(IDalamudPluginInterface pi, string errorMessage)
|
|
||||||
=> GetLog(pi).Error(errorMessage);
|
|
||||||
|
|
||||||
public static IPluginLog GetLog(IDalamudPluginInterface pi)
|
|
||||||
{
|
|
||||||
if (_log != null)
|
|
||||||
return _log;
|
|
||||||
|
|
||||||
_ = new PluginLogHelper(pi);
|
|
||||||
return _log!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.Api.Api;
|
|
||||||
using Glamourer.Api.Enums;
|
|
||||||
using Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.IpcSubscribers;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiDesigns.GetDesignList"/>
|
|
||||||
public sealed class GetDesignList(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<Dictionary<Guid, string>>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetDesignList)}.V2";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiDesigns.GetDesignList"/>
|
|
||||||
public new Dictionary<Guid, string> Invoke()
|
|
||||||
=> base.Invoke();
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<Dictionary<Guid, string>> Provider(IDalamudPluginInterface pi, IGlamourerApiDesigns api)
|
|
||||||
=> new(pi, Label, api.GetDesignList);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiDesigns.ApplyDesign"/>
|
|
||||||
public sealed class ApplyDesign(IDalamudPluginInterface pi) : FuncSubscriber<Guid, int, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyDesign)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiDesigns.ApplyDesign"/>
|
|
||||||
public GlamourerApiEc Invoke(Guid designId, int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.DesignDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(designId, objectIndex, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<Guid, int, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiDesigns api)
|
|
||||||
=> new(pi, Label, (a, b, c, d) => (int)api.ApplyDesign(a, b, c, (ApplyFlag)d));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiDesigns.ApplyDesignName"/>
|
|
||||||
public sealed class ApplyDesignName(IDalamudPluginInterface pi) : FuncSubscriber<Guid, string, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyDesignName)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiDesigns.ApplyDesignName"/>
|
|
||||||
public GlamourerApiEc Invoke(Guid designId, string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.DesignDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(designId, objectName, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<Guid, string, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiDesigns api)
|
|
||||||
=> new(pi, Label, (a, b, c, d) => (int)api.ApplyDesignName(a, b, c, (ApplyFlag)d));
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.Api.Api;
|
|
||||||
using Glamourer.Api.Enums;
|
|
||||||
using Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.IpcSubscribers;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetItem"/>
|
|
||||||
public sealed class SetItem(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<int, byte, ulong, IReadOnlyList<byte>, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetItem)}.V3";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetItem"/>
|
|
||||||
public GlamourerApiEc Invoke(int objectIndex, ApiEquipSlot slot, ulong itemId, IReadOnlyList<byte> stain, uint key = 0,
|
|
||||||
ApplyFlag flags = ApplyFlag.Once)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectIndex, (byte)slot, itemId, stain, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<int, byte, ulong, IReadOnlyList<byte>, uint, ulong, int> Provider(IDalamudPluginInterface pi,
|
|
||||||
IGlamourerApiItems api)
|
|
||||||
=> new(pi, Label, (a, b, c, d, e, f) => (int)api.SetItem(a, (ApiEquipSlot)b, c, d, e, (ApplyFlag)f));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetItemName"/>
|
|
||||||
public sealed class SetItemName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, byte, ulong, IReadOnlyList<byte>, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetItemName)}.V2";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetItem"/>
|
|
||||||
public GlamourerApiEc Invoke(string objectName, ApiEquipSlot slot, ulong itemId, IReadOnlyList<byte> stain, uint key = 0,
|
|
||||||
ApplyFlag flags = ApplyFlag.Once)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectName, (byte)slot, itemId, stain, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<string, byte, ulong, IReadOnlyList<byte>, uint, ulong, int> Provider(IDalamudPluginInterface pi,
|
|
||||||
IGlamourerApiItems api)
|
|
||||||
=> new(pi, Label, (a, b, c, d, e, f) => (int)api.SetItemName(a, (ApiEquipSlot)b, c, d, e, (ApplyFlag)f));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetBonusItem"/>
|
|
||||||
public sealed class SetBonusItem(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<int, byte, ulong, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetBonusItem)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetBonusItem"/>
|
|
||||||
public GlamourerApiEc Invoke(int objectIndex, ApiBonusSlot slot, ulong itemId, uint key = 0, ApplyFlag flags = ApplyFlag.Once)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectIndex, (byte)slot, itemId, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<int, byte, ulong, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiItems api)
|
|
||||||
=> new(pi, Label, (a, b, c, d, e) => (int)api.SetBonusItem(a, (ApiBonusSlot)b, c, d, (ApplyFlag)e));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetBonusItemName"/>
|
|
||||||
public sealed class SetBonusItemName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, byte, ulong, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetBonusItemName)}.V2";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetBonusItemName"/>
|
|
||||||
public GlamourerApiEc Invoke(string objectName, ApiBonusSlot slot, ulong itemId, uint key = 0, ApplyFlag flags = ApplyFlag.Once)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectName, (byte)slot, itemId, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<string, byte, ulong, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiItems api)
|
|
||||||
=> new(pi, Label, (a, b, c, d, e) => (int)api.SetBonusItemName(a, (ApiBonusSlot)b, c, d, (ApplyFlag)e));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetMetaState"/>
|
|
||||||
public sealed class SetMetaState(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<int, ulong, bool, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetMetaState)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetMetaState"/>
|
|
||||||
public GlamourerApiEc Invoke(int objectIndex, MetaFlag types, bool newValue, uint key = 0,
|
|
||||||
ApplyFlag flags = ApplyFlag.Once)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectIndex, (ulong)types, newValue, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<int, ulong, bool, uint, ulong, int> Provider(IDalamudPluginInterface pi,
|
|
||||||
IGlamourerApiItems api)
|
|
||||||
=> new(pi, Label, (a, b, c, d, e) => (int)api.SetMetaState(a, (MetaFlag)b, c, d, (ApplyFlag)e));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetMetaStateName"/>
|
|
||||||
public sealed class SetMetaStateName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, ulong, bool, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetMetaStateName)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiItems.SetMetaStateName"/>
|
|
||||||
public GlamourerApiEc Invoke(string objectName, MetaFlag types, bool newValue, uint key = 0,
|
|
||||||
ApplyFlag flags = ApplyFlag.Once)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectName, (ulong)types, newValue, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<string, ulong, bool, uint, ulong, int> Provider(IDalamudPluginInterface pi,
|
|
||||||
IGlamourerApiItems api)
|
|
||||||
=> new(pi, Label, (a, b, c, d, e) => (int)api.SetMetaStateName(a, (MetaFlag)b, c, d, (ApplyFlag)e));
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
|
||||||
|
|
||||||
namespace Glamourer.Api.IpcSubscribers.Legacy;
|
|
||||||
|
|
||||||
public sealed class GetDesignList(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<(string Name, Guid Identifier)[]>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetDesignList)}";
|
|
||||||
|
|
||||||
public new (string Name, Guid Identifier)[] Invoke()
|
|
||||||
=> base.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyByGuid(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<Guid, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyByGuid)}";
|
|
||||||
|
|
||||||
public new void Invoke(Guid design, string name)
|
|
||||||
=> base.Invoke(design, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyByGuidOnce(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<Guid, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyByGuidOnce)}";
|
|
||||||
|
|
||||||
public new void Invoke(Guid design, string name)
|
|
||||||
=> base.Invoke(design, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyByGuidToCharacter(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<Guid, ICharacter?>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyByGuidToCharacter)}";
|
|
||||||
|
|
||||||
public new void Invoke(Guid design, ICharacter? character)
|
|
||||||
=> base.Invoke(design, character);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyByGuidOnceToCharacter(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<Guid, ICharacter?>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyByGuidOnceToCharacter)}";
|
|
||||||
|
|
||||||
public new void Invoke(Guid design, ICharacter? character)
|
|
||||||
=> base.Invoke(design, character);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.Api.Api;
|
|
||||||
using Glamourer.Api.Enums;
|
|
||||||
using Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
|
||||||
|
|
||||||
namespace Glamourer.Api.IpcSubscribers.Legacy;
|
|
||||||
|
|
||||||
public sealed class SetItem(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<ICharacter?, byte, ulong, byte, uint, int>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetItem)}";
|
|
||||||
|
|
||||||
public new GlamourerApiEc Invoke(ICharacter? character, byte slot, ulong itemId, byte stainId, uint key)
|
|
||||||
=> (GlamourerApiEc)base.Invoke(character, slot, itemId, stainId, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetItemOnce(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<ICharacter?, byte, ulong, byte, uint, int>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetItemOnce)}";
|
|
||||||
|
|
||||||
public new GlamourerApiEc Invoke(ICharacter? character, byte slot, ulong itemId, byte stainId, uint key)
|
|
||||||
=> (GlamourerApiEc)base.Invoke(character, slot, itemId, stainId, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetItemByActorName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, byte, ulong, byte, uint, int>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetItemByActorName)}";
|
|
||||||
|
|
||||||
public new GlamourerApiEc Invoke(string actorName, byte slot, ulong itemId, byte stainId, uint key)
|
|
||||||
=> (GlamourerApiEc)base.Invoke(actorName, slot, itemId, stainId, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetItemOnceByActorName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, byte, ulong, byte, uint, int>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetItemOnceByActorName)}";
|
|
||||||
|
|
||||||
public new GlamourerApiEc Invoke(string actorName, byte slot, ulong itemId, byte stainId, uint key)
|
|
||||||
=> (GlamourerApiEc)base.Invoke(actorName, slot, itemId, stainId, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetItemV2(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<int, byte, ulong, byte, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetItem)}.V2";
|
|
||||||
|
|
||||||
public GlamourerApiEc Invoke(int objectIndex, ApiEquipSlot slot, ulong itemId, byte stain, uint key = 0, ApplyFlag flags = ApplyFlag.Once)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectIndex, (byte)slot, itemId, stain, key, (ulong)flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetItemName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, byte, ulong, byte, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(SetItemName)}";
|
|
||||||
|
|
||||||
public GlamourerApiEc Invoke(string objectName, ApiEquipSlot slot, ulong itemId, byte stain, uint key = 0, ApplyFlag flags = ApplyFlag.Once)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectName, (byte)slot, itemId, stain, key, (ulong)flags);
|
|
||||||
|
|
||||||
public static FuncProvider<string, byte, ulong, byte, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiItems api)
|
|
||||||
=> new(pi, Label, (a, b, c, d, e, f) => (int)api.SetItemName(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f));
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
|
||||||
|
|
||||||
namespace Glamourer.Api.IpcSubscribers.Legacy;
|
|
||||||
|
|
||||||
public sealed class ApiVersions(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<(int, int)>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApiVersions)}";
|
|
||||||
|
|
||||||
public new (int Major, int Minor) Invoke()
|
|
||||||
=> base.Invoke();
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
|
||||||
|
|
||||||
namespace Glamourer.Api.IpcSubscribers.Legacy;
|
|
||||||
|
|
||||||
public sealed class Revert(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(Revert)}";
|
|
||||||
|
|
||||||
public new void Invoke(string characterName)
|
|
||||||
=> base.Invoke(characterName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class RevertCharacter(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<ICharacter?>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(RevertCharacter)}";
|
|
||||||
|
|
||||||
public new void Invoke(ICharacter? character)
|
|
||||||
=> base.Invoke(character);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class RevertLock(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<string, uint>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(RevertLock)}";
|
|
||||||
|
|
||||||
public new void Invoke(string characterName, uint key)
|
|
||||||
=> base.Invoke(characterName, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class RevertCharacterLock(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<ICharacter?, uint>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(RevertCharacterLock)}";
|
|
||||||
|
|
||||||
public new void Invoke(ICharacter? character, uint key)
|
|
||||||
=> base.Invoke(character, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class RevertToAutomation(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, uint, bool>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(RevertToAutomation)}";
|
|
||||||
|
|
||||||
public new bool Invoke(string characterName, uint key)
|
|
||||||
=> base.Invoke(characterName, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class RevertToAutomationCharacter(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<ICharacter?, uint, bool>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(RevertToAutomationCharacter)}";
|
|
||||||
|
|
||||||
public new bool Invoke(ICharacter? character, uint key)
|
|
||||||
=> base.Invoke(character, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class Unlock(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<ICharacter?, uint, bool>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(Unlock)}";
|
|
||||||
|
|
||||||
public new bool Invoke(ICharacter? character, uint key)
|
|
||||||
=> base.Invoke(character, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class UnlockName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, uint, bool>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(UnlockName)}";
|
|
||||||
|
|
||||||
public new bool Invoke(string characterName, uint key)
|
|
||||||
=> base.Invoke(characterName, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class StateChanged
|
|
||||||
{
|
|
||||||
public const string Label = $"Penumbra.{nameof(StateChanged)}";
|
|
||||||
|
|
||||||
public static EventSubscriber<int, nint, Lazy<string>> Subscriber(IDalamudPluginInterface pi,
|
|
||||||
params Action<int, nint, Lazy<string>>[] actions)
|
|
||||||
=> new(pi, Label, actions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GetAllCustomization(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, string?>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetAllCustomization)}";
|
|
||||||
|
|
||||||
public new string? Invoke(string characterName)
|
|
||||||
=> base.Invoke(characterName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GetAllCustomizationFromCharacter(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<ICharacter?, string?>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetAllCustomizationFromCharacter)}";
|
|
||||||
|
|
||||||
public new string? Invoke(ICharacter? character)
|
|
||||||
=> base.Invoke(character);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GetAllCustomizationLocked(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, uint, string?>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetAllCustomizationLocked)}";
|
|
||||||
|
|
||||||
public new string? Invoke(string characterName, uint key)
|
|
||||||
=> base.Invoke(characterName, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GetAllCustomizationFromLockedCharacter(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<ICharacter?, uint, string?>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetAllCustomizationFromLockedCharacter)}";
|
|
||||||
|
|
||||||
public new string? Invoke(ICharacter? character, uint key)
|
|
||||||
=> base.Invoke(character, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyAll(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<string, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyAll)}";
|
|
||||||
|
|
||||||
public new void Invoke(string characterName, string stateBase64)
|
|
||||||
=> base.Invoke(characterName, stateBase64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyAllOnce(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<string, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyAllOnce)}";
|
|
||||||
|
|
||||||
public new void Invoke(string characterName, string stateBase64)
|
|
||||||
=> base.Invoke(characterName, stateBase64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyAllToCharacter(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<ICharacter?, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyAllToCharacter)}";
|
|
||||||
|
|
||||||
public new void Invoke(ICharacter? character, string stateBase64)
|
|
||||||
=> base.Invoke(character, stateBase64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyAllOnceToCharacter(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<ICharacter?, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyAllOnceToCharacter)}";
|
|
||||||
|
|
||||||
public new void Invoke(ICharacter? character, string stateBase64)
|
|
||||||
=> base.Invoke(character, stateBase64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyOnlyEquipment(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<string, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyOnlyEquipment)}";
|
|
||||||
|
|
||||||
public new void Invoke(string characterName, string stateBase64)
|
|
||||||
=> base.Invoke(characterName, stateBase64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyOnlyEquipmentToCharacter(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<ICharacter?, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyOnlyEquipmentToCharacter)}";
|
|
||||||
|
|
||||||
public new void Invoke(ICharacter? character, string stateBase64)
|
|
||||||
=> base.Invoke(character, stateBase64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyOnlyCustomization(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<string, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyOnlyCustomization)}";
|
|
||||||
|
|
||||||
public new void Invoke(string characterName, string stateBase64)
|
|
||||||
=> base.Invoke(characterName, stateBase64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyOnlyCustomizationToCharacter(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<ICharacter?, string>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyOnlyCustomizationToCharacter)}";
|
|
||||||
|
|
||||||
public new void Invoke(ICharacter? character, string stateBase64)
|
|
||||||
=> base.Invoke(character, stateBase64);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyAllLock(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<string, string, uint>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyAllLock)}";
|
|
||||||
|
|
||||||
public new void Invoke(string characterName, string stateBase64, uint key)
|
|
||||||
=> base.Invoke(characterName, stateBase64, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyAllToCharacterLock(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<ICharacter?, string, uint>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyAllToCharacterLock)}";
|
|
||||||
|
|
||||||
public new void Invoke(ICharacter? character, string stateBase64, uint key)
|
|
||||||
=> base.Invoke(character, stateBase64, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyOnlyEquipmentLock(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<string, string, uint>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyOnlyEquipmentLock)}";
|
|
||||||
|
|
||||||
public new void Invoke(string characterName, string stateBase64, uint key)
|
|
||||||
=> base.Invoke(characterName, stateBase64, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyOnlyEquipmentToCharacterLock(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<ICharacter?, string, uint>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyOnlyEquipmentToCharacterLock)}";
|
|
||||||
|
|
||||||
public new void Invoke(ICharacter? character, string stateBase64, uint key)
|
|
||||||
=> base.Invoke(character, stateBase64, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyOnlyCustomizationLock(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<string, string, uint>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyOnlyCustomizationLock)}";
|
|
||||||
|
|
||||||
public new void Invoke(string characterName, string stateBase64, uint key)
|
|
||||||
=> base.Invoke(characterName, stateBase64, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ApplyOnlyCustomizationToCharacterLock(IDalamudPluginInterface pi)
|
|
||||||
: ActionSubscriber<ICharacter?, string, uint>(pi, Label)
|
|
||||||
{
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyOnlyCustomizationToCharacterLock)}";
|
|
||||||
|
|
||||||
public new void Invoke(ICharacter? character, string stateBase64, uint key)
|
|
||||||
=> base.Invoke(character, stateBase64, key);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.Api.Api;
|
|
||||||
using Glamourer.Api.Helpers;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.IpcSubscribers;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiBase.ApiVersion"/>
|
|
||||||
public sealed class ApiVersion(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<(int, int)>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApiVersion)}.V2";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiBase.ApiVersion"/>
|
|
||||||
public new (int Major, int Minor) Invoke()
|
|
||||||
=> base.Invoke();
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<(int, int)> Provider(IDalamudPluginInterface pi, IGlamourerApiBase api)
|
|
||||||
=> new(pi, Label, () => api.ApiVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Triggered when the Glamourer API is initialized and ready. </summary>
|
|
||||||
public static class Initialized
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(Initialized)}";
|
|
||||||
|
|
||||||
/// <summary> Create a new event subscriber. </summary>
|
|
||||||
public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions)
|
|
||||||
=> new(pi, Label, actions);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static EventProvider Provider(IDalamudPluginInterface pi)
|
|
||||||
=> new(pi, Label);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Triggered when the Glamourer API is fully disposed and unavailable. </summary>
|
|
||||||
public static class Disposed
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(Disposed)}";
|
|
||||||
|
|
||||||
/// <summary> Create a new event subscriber. </summary>
|
|
||||||
public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions)
|
|
||||||
=> new(pi, Label, actions);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static EventProvider Provider(IDalamudPluginInterface pi)
|
|
||||||
=> new(pi, Label);
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.Api.Api;
|
|
||||||
using Glamourer.Api.Enums;
|
|
||||||
using Glamourer.Api.Helpers;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace Glamourer.Api.IpcSubscribers;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.GetState"/>
|
|
||||||
public sealed class GetState(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<int, uint, (int, JObject?)>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetState)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.GetState"/>
|
|
||||||
public new (GlamourerApiEc, JObject?) Invoke(int objectIndex, uint key = 0)
|
|
||||||
{
|
|
||||||
var (ec, data) = base.Invoke(objectIndex, key);
|
|
||||||
return ((GlamourerApiEc)ec, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<int, uint, (int, JObject?)> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b) =>
|
|
||||||
{
|
|
||||||
var (ec, data) = api.GetState(a, b);
|
|
||||||
return ((int)ec, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.GetStateName"/>
|
|
||||||
public sealed class GetStateName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, uint, (int, JObject?)>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetStateName)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.GetStateName"/>
|
|
||||||
public new (GlamourerApiEc, JObject?) Invoke(string objectName, uint key = 0)
|
|
||||||
{
|
|
||||||
var (ec, data) = base.Invoke(objectName, key);
|
|
||||||
return ((GlamourerApiEc)ec, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<string, uint, (int, JObject?)> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (i, k) =>
|
|
||||||
{
|
|
||||||
var (ec, data) = api.GetStateName(i, k);
|
|
||||||
return ((int)ec, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.GetStateBase64"/>
|
|
||||||
public sealed class GetStateBase64(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<int, uint, (int, string?)>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetStateBase64)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.GetStateBase64"/>
|
|
||||||
public new (GlamourerApiEc, string?) Invoke(int objectIndex, uint key = 0)
|
|
||||||
{
|
|
||||||
var (ec, data) = base.Invoke(objectIndex, key);
|
|
||||||
return ((GlamourerApiEc)ec, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<int, uint, (int, string?)> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b) =>
|
|
||||||
{
|
|
||||||
var (ec, data) = api.GetStateBase64(a, b);
|
|
||||||
return ((int)ec, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.GetStateBase64Name"/>
|
|
||||||
public sealed class GetStateBase64Name(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, uint, (int, string?)>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(GetStateBase64Name)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.GetStateBase64Name"/>
|
|
||||||
public new (GlamourerApiEc, string?) Invoke(string objectName, uint key = 0)
|
|
||||||
{
|
|
||||||
var (ec, data) = base.Invoke(objectName, key);
|
|
||||||
return ((GlamourerApiEc)ec, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<string, uint, (int, string?)> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (i, k) =>
|
|
||||||
{
|
|
||||||
var (ec, data) = api.GetStateBase64Name(i, k);
|
|
||||||
return ((int)ec, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.ApplyState"/>
|
|
||||||
public sealed class ApplyState(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<object, int, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyState)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.ApplyState"/>
|
|
||||||
public GlamourerApiEc Invoke(JObject state, int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.StateDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(state, objectIndex, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.ApplyState"/>
|
|
||||||
public GlamourerApiEc Invoke(string base64State, int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.StateDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(base64State, objectIndex, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<object, int, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b, c, d) => (int)api.ApplyState(a, b, c, (ApplyFlag)d));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.ApplyStateName"/>
|
|
||||||
public sealed class ApplyStateName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<object, string, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(ApplyStateName)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.ApplyState"/>
|
|
||||||
public GlamourerApiEc Invoke(JObject state, string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.StateDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(state, objectName, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.ApplyState"/>
|
|
||||||
public GlamourerApiEc Invoke(string base64State, string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.StateDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(base64State, objectName, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<object, string, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b, c, d) => (int)api.ApplyStateName(a, b, c, (ApplyFlag)d));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.RevertState"/>
|
|
||||||
public sealed class RevertState(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<int, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(RevertState)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.RevertState"/>
|
|
||||||
public GlamourerApiEc Invoke(int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.RevertDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectIndex, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<int, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b, c) => (int)api.RevertState(a, b, (ApplyFlag)c));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.RevertStateName"/>
|
|
||||||
public sealed class RevertStateName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(RevertStateName)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.RevertStateName"/>
|
|
||||||
public GlamourerApiEc Invoke(string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.RevertDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectName, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<string, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b, c) => (int)api.RevertStateName(a, b, (ApplyFlag)c));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.UnlockState"/>
|
|
||||||
public sealed class UnlockState(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<int, uint, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(UnlockState)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.UnlockState"/>
|
|
||||||
public new GlamourerApiEc Invoke(int objectIndex, uint key = 0)
|
|
||||||
=> (GlamourerApiEc)base.Invoke(objectIndex, key);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<int, uint, int> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b) => (int)api.UnlockState(a, b));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.UnlockStateName"/>
|
|
||||||
public sealed class UnlockStateName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, uint, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(UnlockStateName)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.UnlockStateName"/>
|
|
||||||
public new GlamourerApiEc Invoke(string objectName, uint key = 0)
|
|
||||||
=> (GlamourerApiEc)base.Invoke(objectName, key);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<string, uint, int> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b) => (int)api.UnlockStateName(a, b));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.UnlockAll"/>
|
|
||||||
public sealed class UnlockAll(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<uint, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(UnlockAll)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.UnlockAll"/>
|
|
||||||
public new int Invoke(uint key)
|
|
||||||
=> base.Invoke(key);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<uint, int> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, api.UnlockAll);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.RevertToAutomation"/>
|
|
||||||
public sealed class RevertToAutomation(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<int, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(RevertToAutomation)}.V2";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.RevertToAutomation"/>
|
|
||||||
public GlamourerApiEc Invoke(int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.RevertDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectIndex, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<int, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b, c) => (int)api.RevertToAutomation(a, b, (ApplyFlag)c));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.RevertToAutomationName"/>
|
|
||||||
public sealed class RevertToAutomationName(IDalamudPluginInterface pi)
|
|
||||||
: FuncSubscriber<string, uint, ulong, int>(pi, Label)
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Glamourer.{nameof(RevertToAutomationName)}";
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.RevertToAutomationName"/>
|
|
||||||
public GlamourerApiEc Invoke(string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.RevertDefault)
|
|
||||||
=> (GlamourerApiEc)Invoke(objectName, key, (ulong)flags);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static FuncProvider<string, uint, ulong, int> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (a, b, c) => (int)api.RevertToAutomationName(a, b, (ApplyFlag)c));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.StateChanged" />
|
|
||||||
public static class StateChanged
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Penumbra.{nameof(StateChanged)}.V2";
|
|
||||||
|
|
||||||
/// <summary> Create a new event subscriber. </summary>
|
|
||||||
public static EventSubscriber<nint> Subscriber(IDalamudPluginInterface pi, params Action<nint>[] actions)
|
|
||||||
=> new(pi, Label, actions);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static EventProvider<nint> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (t => api.StateChanged += t, t => api.StateChanged -= t));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.StateChangedWithType" />
|
|
||||||
public static class StateChangedWithType
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Penumbra.{nameof(StateChangedWithType)}";
|
|
||||||
|
|
||||||
/// <summary> Create a new event subscriber. </summary>
|
|
||||||
public static EventSubscriber<nint, StateChangeType> Subscriber(IDalamudPluginInterface pi, params Action<nint, StateChangeType>[] actions)
|
|
||||||
=> new(pi, Label, actions);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static EventProvider<nint, StateChangeType> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (t => api.StateChangedWithType += t, t => api.StateChangedWithType -= t));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.StateFinalized" />
|
|
||||||
public static class StateFinalized
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Penumbra.{nameof(StateFinalized)}";
|
|
||||||
|
|
||||||
/// <summary> Create a new event subscriber. </summary>
|
|
||||||
public static EventSubscriber<nint, StateFinalizationType> Subscriber(IDalamudPluginInterface pi, params Action<nint, StateFinalizationType>[] actions)
|
|
||||||
=> new(pi, Label, actions);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static EventProvider<nint, StateFinalizationType> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (t => api.StateFinalized += t, t => api.StateFinalized -= t));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IGlamourerApiState.GPoseChanged" />
|
|
||||||
public static class GPoseChanged
|
|
||||||
{
|
|
||||||
/// <summary> The label. </summary>
|
|
||||||
public const string Label = $"Penumbra.{nameof(GPoseChanged)}";
|
|
||||||
|
|
||||||
/// <summary> Create a new event subscriber. </summary>
|
|
||||||
public static EventSubscriber<bool> Subscriber(IDalamudPluginInterface pi, params Action<bool>[] actions)
|
|
||||||
=> new(pi, Label, actions);
|
|
||||||
|
|
||||||
/// <summary> Create a provider. </summary>
|
|
||||||
public static EventProvider<bool> Provider(IDalamudPluginInterface pi, IGlamourerApiState api)
|
|
||||||
=> new(pi, Label, (t => api.GPoseChanged += t, t => api.GPoseChanged -= t));
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Glamourer
|
|
||||||
|
|
||||||
This is an auxiliary repository for Glamourers external API.
|
|
||||||
For more information, see the [main repo](https://github.com/Ottermandias/Glamourer).
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"dependencies": {
|
|
||||||
"net9.0-windows7.0": {
|
|
||||||
"DotNet.ReproducibleBuilds": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[1.2.25, )",
|
|
||||||
"resolved": "1.2.25",
|
|
||||||
"contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022 Mare Synchronos
|
Copyright (c) 2022 Penumbra-Sync
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
1
MareAPI
Submodule
1
MareAPI
Submodule
Submodule MareAPI added at d105d20507
@@ -5,7 +5,7 @@ VisualStudioVersion = 17.1.32328.378
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos", "MareSynchronos\MareSynchronos.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos", "MareSynchronos\MareSynchronos.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.Interop.Ipc;
|
using System;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
@@ -606,14 +607,35 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
_scanCancellationTokenSource?.Cancel();
|
try
|
||||||
|
{
|
||||||
|
_scanCancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
_scanCancellationTokenSource.Dispose();
|
||||||
PenumbraWatcher?.Dispose();
|
PenumbraWatcher?.Dispose();
|
||||||
MareWatcher?.Dispose();
|
MareWatcher?.Dispose();
|
||||||
SubstWatcher?.Dispose();
|
SubstWatcher?.Dispose();
|
||||||
_penumbraFswCts?.CancelDispose();
|
TryCancelAndDispose(_penumbraFswCts);
|
||||||
_mareFswCts?.CancelDispose();
|
TryCancelAndDispose(_mareFswCts);
|
||||||
_substFswCts?.CancelDispose();
|
TryCancelAndDispose(_substFswCts);
|
||||||
_periodicCalculationTokenSource?.CancelDispose();
|
TryCancelAndDispose(_periodicCalculationTokenSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCancelAndDispose(CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FullFileScan(CancellationToken ct)
|
private void FullFileScan(CancellationToken ct)
|
||||||
@@ -856,4 +878,4 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
StartPenumbraWatcher(penumbraDir);
|
StartPenumbraWatcher(penumbraDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
foreach (var entry in cleanedPaths)
|
foreach (var entry in cleanedPaths)
|
||||||
{
|
{
|
||||||
//_logger.LogDebug("Checking {path}", entry.Value);
|
|
||||||
|
|
||||||
if (dict.TryGetValue(entry.Value, out var entity))
|
if (dict.TryGetValue(entry.Value, out var entity))
|
||||||
{
|
{
|
||||||
@@ -366,8 +365,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
|
if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
//_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
|
entries.Add(fileCache);
|
||||||
entries.Add(fileCache);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +387,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
|
private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
|
||||||
{
|
{
|
||||||
var resultingFileCache = ReplacePathPrefixes(fileCache);
|
var resultingFileCache = ReplacePathPrefixes(fileCache);
|
||||||
//_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
|
|
||||||
resultingFileCache = Validate(resultingFileCache);
|
resultingFileCache = Validate(resultingFileCache);
|
||||||
return resultingFileCache;
|
return resultingFileCache;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public sealed class FileCompactor
|
|||||||
|
|
||||||
private readonly Dictionary<string, int> _clusterSizes;
|
private readonly Dictionary<string, int> _clusterSizes;
|
||||||
|
|
||||||
private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo;
|
private readonly WofFileCompressionInfoV1 _efInfo;
|
||||||
private readonly ILogger<FileCompactor> _logger;
|
private readonly ILogger<FileCompactor> _logger;
|
||||||
|
|
||||||
private readonly MareConfigService _mareConfigService;
|
private readonly MareConfigService _mareConfigService;
|
||||||
@@ -24,7 +24,7 @@ public sealed class FileCompactor
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mareConfigService = mareConfigService;
|
_mareConfigService = mareConfigService;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_efInfo = new WOF_FILE_COMPRESSION_INFO_V1
|
_efInfo = new WofFileCompressionInfoV1
|
||||||
{
|
{
|
||||||
Algorithm = CompressionAlgorithm.XPRESS8K,
|
Algorithm = CompressionAlgorithm.XPRESS8K,
|
||||||
Flags = 0
|
Flags = 0
|
||||||
@@ -123,7 +123,7 @@ public sealed class FileCompactor
|
|||||||
out uint lpTotalNumberOfClusters);
|
out uint lpTotalNumberOfClusters);
|
||||||
|
|
||||||
[DllImport("WoFUtil.dll")]
|
[DllImport("WoFUtil.dll")]
|
||||||
private static extern int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WOF_FILE_COMPRESSION_INFO_V1 Info, ref uint BufferLength);
|
private static extern int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WofFileCompressionInfoV1 Info, ref uint BufferLength);
|
||||||
|
|
||||||
[DllImport("WofUtil.dll")]
|
[DllImport("WofUtil.dll")]
|
||||||
private static extern int WofSetFileDataLocation(IntPtr FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length);
|
private static extern int WofSetFileDataLocation(IntPtr FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length);
|
||||||
@@ -242,9 +242,9 @@ public sealed class FileCompactor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
private struct WOF_FILE_COMPRESSION_INFO_V1
|
private struct WofFileCompressionInfoV1
|
||||||
{
|
{
|
||||||
public CompressionAlgorithm Algorithm;
|
public CompressionAlgorithm Algorithm;
|
||||||
public ulong Flags;
|
public ulong Flags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ namespace MareSynchronos.Interop;
|
|||||||
|
|
||||||
public record ChatChannelOverride
|
public record ChatChannelOverride
|
||||||
{
|
{
|
||||||
public string ChannelName = string.Empty;
|
public string ChannelName { get; set; } = string.Empty;
|
||||||
public Action<byte[]>? ChatMessageHandler;
|
public Action<byte[]>? ChatMessageHandler { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe sealed class GameChatHooks : IDisposable
|
public unsafe sealed class GameChatHooks : IDisposable
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ public class MdlFile
|
|||||||
public ushort Unknown9;
|
public ushort Unknown9;
|
||||||
|
|
||||||
// Offsets are stored relative to RuntimeSize instead of file start.
|
// Offsets are stored relative to RuntimeSize instead of file start.
|
||||||
public uint[] VertexOffset = [0, 0, 0];
|
public uint[] VertexOffset;
|
||||||
public uint[] IndexOffset = [0, 0, 0];
|
public uint[] IndexOffset;
|
||||||
|
|
||||||
public uint[] VertexBufferSize = [0, 0, 0];
|
public uint[] VertexBufferSize;
|
||||||
public uint[] IndexBufferSize = [0, 0, 0];
|
public uint[] IndexBufferSize;
|
||||||
public byte LodCount;
|
public byte LodCount;
|
||||||
public bool EnableIndexBufferStreaming;
|
public bool EnableIndexBufferStreaming;
|
||||||
public bool EnableEdgeGeometry;
|
public bool EnableEdgeGeometry;
|
||||||
@@ -43,15 +43,26 @@ public class MdlFile
|
|||||||
public ModelFlags1 Flags1;
|
public ModelFlags1 Flags1;
|
||||||
public ModelFlags2 Flags2;
|
public ModelFlags2 Flags2;
|
||||||
|
|
||||||
public VertexDeclarationStruct[] VertexDeclarations = [];
|
public VertexDeclarationStruct[] VertexDeclarations;
|
||||||
public ElementIdStruct[] ElementIds = [];
|
public ElementIdStruct[] ElementIds;
|
||||||
public MeshStruct[] Meshes = [];
|
public MeshStruct[] Meshes;
|
||||||
public BoundingBoxStruct[] BoneBoundingBoxes = [];
|
public BoundingBoxStruct[] BoneBoundingBoxes;
|
||||||
public LodStruct[] Lods = [];
|
public LodStruct[] Lods;
|
||||||
public ExtraLodStruct[] ExtraLods = [];
|
public ExtraLodStruct[] ExtraLods;
|
||||||
|
|
||||||
public MdlFile(string filePath)
|
public MdlFile(string filePath)
|
||||||
{
|
{
|
||||||
|
VertexOffset = Array.Empty<uint>();
|
||||||
|
IndexOffset = Array.Empty<uint>();
|
||||||
|
VertexBufferSize = Array.Empty<uint>();
|
||||||
|
IndexBufferSize = Array.Empty<uint>();
|
||||||
|
VertexDeclarations = Array.Empty<VertexDeclarationStruct>();
|
||||||
|
ElementIds = Array.Empty<ElementIdStruct>();
|
||||||
|
Meshes = Array.Empty<MeshStruct>();
|
||||||
|
BoneBoundingBoxes = Array.Empty<BoundingBoxStruct>();
|
||||||
|
Lods = Array.Empty<LodStruct>();
|
||||||
|
ExtraLods = Array.Empty<ExtraLodStruct>();
|
||||||
|
|
||||||
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||||
using var r = new LuminaBinaryReader(stream);
|
using var r = new LuminaBinaryReader(stream);
|
||||||
|
|
||||||
@@ -256,4 +267,4 @@ public class MdlFile
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma warning restore S1104 // Fields should not have public accessibility
|
#pragma warning restore S1104 // Fields should not have public accessibility
|
||||||
|
|||||||
@@ -95,8 +95,6 @@ public sealed class IpcCallerBrio : IIpcCaller
|
|||||||
if (gameObject == null) return default;
|
if (gameObject == null) return default;
|
||||||
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
|
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||||
if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default;
|
if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default;
|
||||||
//_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue);
|
|
||||||
|
|
||||||
return new WorldData()
|
return new WorldData()
|
||||||
{
|
{
|
||||||
PositionX = data.Item1.Value.X,
|
PositionX = data.Item1.Value.X,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using System;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
@@ -29,7 +30,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
private bool _marePluginEnabled = false;
|
private bool _marePluginEnabled = false;
|
||||||
private bool _impersonating = false;
|
private bool _impersonating = false;
|
||||||
private DateTime _unregisterTime = DateTime.UtcNow;
|
private DateTime _unregisterTime = DateTime.UtcNow;
|
||||||
private CancellationTokenSource _registerDelayCts = new();
|
private CancellationTokenSource? _registerDelayCts = new();
|
||||||
|
|
||||||
public bool MarePluginEnabled => _marePluginEnabled;
|
public bool MarePluginEnabled => _marePluginEnabled;
|
||||||
public bool ImpersonationActive => _impersonating;
|
public bool ImpersonationActive => _impersonating;
|
||||||
@@ -100,7 +101,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
if (_mareConfig.Current.MareAPI)
|
if (_mareConfig.Current.MareAPI)
|
||||||
{
|
{
|
||||||
var cancelToken = _registerDelayCts.Token;
|
var cancelToken = EnsureFreshCts(ref _registerDelayCts).Token;
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
// Wait before registering to reduce the chance of a race condition
|
// Wait before registering to reduce the chance of a race condition
|
||||||
@@ -125,7 +126,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_registerDelayCts = _registerDelayCts.CancelRecreate();
|
EnsureFreshCts(ref _registerDelayCts);
|
||||||
if (_impersonating)
|
if (_impersonating)
|
||||||
{
|
{
|
||||||
_loadFileProviderMare?.UnregisterFunc();
|
_loadFileProviderMare?.UnregisterFunc();
|
||||||
@@ -146,7 +147,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
_loadFileAsyncProvider?.UnregisterFunc();
|
_loadFileAsyncProvider?.UnregisterFunc();
|
||||||
_handledGameAddresses?.UnregisterFunc();
|
_handledGameAddresses?.UnregisterFunc();
|
||||||
|
|
||||||
_registerDelayCts.Cancel();
|
TryCancel(_registerDelayCts);
|
||||||
if (_impersonating)
|
if (_impersonating)
|
||||||
{
|
{
|
||||||
_loadFileProviderMare?.UnregisterFunc();
|
_loadFileProviderMare?.UnregisterFunc();
|
||||||
@@ -155,6 +156,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mediator.UnsubscribeAll(this);
|
Mediator.UnsubscribeAll(this);
|
||||||
|
CancelAndDispose(ref _registerDelayCts);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,4 +195,31 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
|
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
TryCancel(cts);
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCancel(CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using System;
|
||||||
using MareSynchronos.PlayerData.Handlers;
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Utils;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
namespace MareSynchronos.Interop.Ipc;
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
public class RedrawManager
|
public class RedrawManager : IDisposable
|
||||||
{
|
{
|
||||||
private readonly MareMediator _mareMediator;
|
private readonly MareMediator _mareMediator;
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly ConcurrentDictionary<nint, bool> _penumbraRedrawRequests = [];
|
private readonly ConcurrentDictionary<nint, bool> _penumbraRedrawRequests = [];
|
||||||
private CancellationTokenSource _disposalCts = new();
|
private CancellationTokenSource? _disposalCts = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
public SemaphoreSlim RedrawSemaphore { get; init; } = new(2, 2);
|
public SemaphoreSlim RedrawSemaphore { get; init; } = new(2, 2);
|
||||||
|
|
||||||
@@ -32,12 +33,12 @@ public class RedrawManager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using CancellationTokenSource cancelToken = new CancellationTokenSource();
|
using CancellationTokenSource cancelToken = new CancellationTokenSource();
|
||||||
using CancellationTokenSource combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, token, _disposalCts.Token);
|
using CancellationTokenSource combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, token, EnsureFreshCts(ref _disposalCts).Token);
|
||||||
var combinedToken = combinedCts.Token;
|
var combinedToken = combinedCts.Token;
|
||||||
cancelToken.CancelAfter(TimeSpan.FromSeconds(15));
|
cancelToken.CancelAfter(TimeSpan.FromSeconds(15));
|
||||||
await handler.ActOnFrameworkAfterEnsureNoDrawAsync(action, combinedToken).ConfigureAwait(false);
|
await handler.ActOnFrameworkAfterEnsureNoDrawAsync(action, combinedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!_disposalCts.Token.IsCancellationRequested)
|
if (!_disposalCts!.Token.IsCancellationRequested)
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, handler, applicationId, 30000, combinedToken).ConfigureAwait(false);
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, handler, applicationId, 30000, combinedToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -49,6 +50,45 @@ public class RedrawManager
|
|||||||
|
|
||||||
internal void Cancel()
|
internal void Cancel()
|
||||||
{
|
{
|
||||||
_disposalCts = _disposalCts.CancelRecreate();
|
EnsureFreshCts(ref _disposalCts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref _disposalCts);
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,72 @@
|
|||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MareSynchronos.MareConfiguration;
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger) : IHostedService
|
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, MareConfigService mareConfig) : IHostedService
|
||||||
{
|
{
|
||||||
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||||
|
private readonly MareConfigService _mareConfig = mareConfig;
|
||||||
|
|
||||||
public void Migrate()
|
public void Migrate()
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = _mareConfig.ConfigurationPath;
|
||||||
|
if (!File.Exists(path)) return;
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
if (root.TryGetProperty("EnableAutoSyncDiscovery", out var enableAutoSync))
|
||||||
|
{
|
||||||
|
var val = enableAutoSync.GetBoolean();
|
||||||
|
if (_mareConfig.Current.EnableAutoDetectDiscovery != val)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.EnableAutoDetectDiscovery = val;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.TryGetProperty("AllowAutoSyncPairRequests", out var allowAutoSync))
|
||||||
|
{
|
||||||
|
var val = allowAutoSync.GetBoolean();
|
||||||
|
if (_mareConfig.Current.AllowAutoDetectPairRequests != val)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.AllowAutoDetectPairRequests = val;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.TryGetProperty("AutoSyncMaxDistanceMeters", out var maxDistSync) && maxDistSync.TryGetInt32(out var md))
|
||||||
|
{
|
||||||
|
if (_mareConfig.Current.AutoDetectMaxDistanceMeters != md)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.AutoDetectMaxDistanceMeters = md;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.TryGetProperty("AutoSyncMuteMinutes", out var muteSync) && muteSync.TryGetInt32(out var mm))
|
||||||
|
{
|
||||||
|
if (_mareConfig.Current.AutoDetectMuteMinutes != mm)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.AutoDetectMuteMinutes = mm;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Migrated config: AutoSync -> AutoDetect fields");
|
||||||
|
_mareConfig.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Configuration migration failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ public class CharaDataConfig : IMareConfiguration
|
|||||||
public bool NearbyOwnServerOnly { get; set; } = false;
|
public bool NearbyOwnServerOnly { get; set; } = false;
|
||||||
public bool NearbyIgnoreHousingLimitations { get; set; } = false;
|
public bool NearbyIgnoreHousingLimitations { get; set; } = false;
|
||||||
public bool NearbyDrawWisps { get; set; } = true;
|
public bool NearbyDrawWisps { get; set; } = true;
|
||||||
|
public int NearbyMaxWisps { get; set; } = 20;
|
||||||
public int NearbyDistanceFilter { get; set; } = 100;
|
public int NearbyDistanceFilter { get; set; } = 100;
|
||||||
public bool NearbyShowOwnData { get; set; } = false;
|
public bool NearbyShowOwnData { get; set; } = false;
|
||||||
public bool ShowHelpTexts { get; set; } = true;
|
public bool ShowHelpTexts { get; set; } = true;
|
||||||
public bool NearbyShowAlways { get; set; } = false;
|
public bool NearbyShowAlways { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.MareConfiguration.Models;
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ public class MareConfig : IMareConfiguration
|
|||||||
public bool UseColorsInDtr { get; set; } = true;
|
public bool UseColorsInDtr { get; set; } = true;
|
||||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0x8D37C0u);
|
||||||
public bool UseNameColors { get; set; } = false;
|
public bool UseNameColors { get; set; } = false;
|
||||||
public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu);
|
public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu);
|
||||||
public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u);
|
public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u);
|
||||||
@@ -37,6 +38,7 @@ public class MareConfig : IMareConfiguration
|
|||||||
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||||
public bool OpenPopupOnAdd { get; set; } = true;
|
public bool OpenPopupOnAdd { get; set; } = true;
|
||||||
public int ParallelDownloads { get; set; } = 10;
|
public int ParallelDownloads { get; set; } = 10;
|
||||||
|
public bool EnableDownloadQueue { get; set; } = false;
|
||||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||||
[Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
[Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||||
@@ -59,6 +61,16 @@ public class MareConfig : IMareConfiguration
|
|||||||
public bool ShowUploading { get; set; } = true;
|
public bool ShowUploading { get; set; } = true;
|
||||||
public bool ShowUploadingBigText { get; set; } = true;
|
public bool ShowUploadingBigText { get; set; } = true;
|
||||||
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||||
|
public string LastChangelogVersionSeen { get; set; } = string.Empty;
|
||||||
|
public bool DefaultDisableSounds { get; set; } = false;
|
||||||
|
public bool DefaultDisableAnimations { get; set; } = false;
|
||||||
|
public bool DefaultDisableVfx { get; set; } = false;
|
||||||
|
public Dictionary<string, SyncOverrideEntry> PairSyncOverrides { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, SyncOverrideEntry> GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public bool EnableAutoDetectDiscovery { get; set; } = true;
|
||||||
|
public bool AllowAutoDetectPairRequests { get; set; } = true;
|
||||||
|
public int AutoDetectMaxDistanceMeters { get; set; } = 40;
|
||||||
|
public int AutoDetectMuteMinutes { get; set; } = 5;
|
||||||
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||||
public int TransferBarsHeight { get; set; } = 12;
|
public int TransferBarsHeight { get; set; } = 12;
|
||||||
public bool TransferBarsShowText { get; set; } = true;
|
public bool TransferBarsShowText { get; set; } = true;
|
||||||
@@ -73,6 +85,11 @@ public class MareConfig : IMareConfiguration
|
|||||||
public int ChatLogKind { get; set; } = 1; // XivChatType.Debug
|
public int ChatLogKind { get; set; } = 1; // XivChatType.Debug
|
||||||
public bool ExtraChatAPI { get; set; } = false;
|
public bool ExtraChatAPI { get; set; } = false;
|
||||||
public bool ExtraChatTags { get; set; } = false;
|
public bool ExtraChatTags { get; set; } = false;
|
||||||
|
public bool TypingIndicatorShowOnNameplates { get; set; } = true;
|
||||||
|
public bool TypingIndicatorShowOnPartyList { get; set; } = true;
|
||||||
|
public bool TypingIndicatorEnabled { get; set; } = true;
|
||||||
|
public bool TypingIndicatorShowSelf { get; set; } = true;
|
||||||
|
public TypingIndicatorBubbleSize TypingIndicatorBubbleSize { get; set; } = TypingIndicatorBubbleSize.Large;
|
||||||
|
|
||||||
public bool MareAPI { get; set; } = true;
|
public bool MareAPI { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class NotificationsConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public List<StoredNotification> Notifications { get; set; } = new();
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
}
|
||||||
@@ -8,9 +8,10 @@ public class PlayerPerformanceConfig : IMareConfiguration
|
|||||||
public bool AutoPausePlayersExceedingThresholds { get; set; } = true;
|
public bool AutoPausePlayersExceedingThresholds { get; set; } = true;
|
||||||
public bool NotifyAutoPauseDirectPairs { get; set; } = true;
|
public bool NotifyAutoPauseDirectPairs { get; set; } = true;
|
||||||
public bool NotifyAutoPauseGroupPairs { get; set; } = true;
|
public bool NotifyAutoPauseGroupPairs { get; set; } = true;
|
||||||
|
public bool ShowSelfAnalysisWarnings { get; set; } = true;
|
||||||
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 500;
|
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 500;
|
||||||
public int TrisAutoPauseThresholdThousands { get; set; } = 400;
|
public int TrisAutoPauseThresholdThousands { get; set; } = 400;
|
||||||
public bool IgnoreDirectPairs { get; set; } = true;
|
public bool IgnoreDirectPairs { get; set; } = true;
|
||||||
public TextureShrinkMode TextureShrinkMode { get; set; } = TextureShrinkMode.Default;
|
public TextureShrinkMode TextureShrinkMode { get; set; } = TextureShrinkMode.Default;
|
||||||
public bool TextureShrinkDeleteOriginal { get; set; } = false;
|
public bool TextureShrinkDeleteOriginal { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class StoredNotification
|
||||||
|
{
|
||||||
|
public string Category { get; set; } = string.Empty; // name of enum NotificationCategory
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
13
MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs
Normal file
13
MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class SyncOverrideEntry
|
||||||
|
{
|
||||||
|
public bool? DisableSounds { get; set; }
|
||||||
|
public bool? DisableAnimations { get; set; }
|
||||||
|
public bool? DisableVfx { get; set; }
|
||||||
|
|
||||||
|
public bool IsEmpty => DisableSounds is null && DisableAnimations is null && DisableVfx is null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
public enum TypingIndicatorBubbleSize
|
||||||
|
{
|
||||||
|
Small,
|
||||||
|
Medium,
|
||||||
|
Large
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class NotificationsConfigService : ConfigurationServiceBase<NotificationsConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "notifications.json";
|
||||||
|
|
||||||
|
public NotificationsConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using MareSynchronos.PlayerData.Services;
|
|||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.Interop;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -150,8 +151,12 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
|
|||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<SyncDefaultsService>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatTypingDetectionService>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>();
|
||||||
|
var characterAnalyzer = _runtimeServiceScope.ServiceProvider.GetRequiredService<CharacterAnalyzer>();
|
||||||
|
_ = characterAnalyzer.ComputeAnalysis(print: false);
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
if (_mareConfigService.Current.LogLevel != LogLevel.Information)
|
if (_mareConfigService.Current.LogLevel != LogLevel.Information)
|
||||||
@@ -167,4 +172,4 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
|
|||||||
Logger?.LogCritical(ex, "Error during launch of managers");
|
Logger?.LogCritical(ex, "Error during launch of managers");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>UmbraSync</AssemblyName>
|
<AssemblyName>UmbraSync</AssemblyName>
|
||||||
<RootNamespace>UmbraSync</RootNamespace>
|
<RootNamespace>UmbraSync</RootNamespace>
|
||||||
<Version>0.0.6</Version>
|
<Version>0.1.9.9</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -50,10 +50,12 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SourceRevisionId>build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ"))</SourceRevisionId>
|
<SourceRevisionId>build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ"))</SourceRevisionId>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DisablePackageVulnerabilityAnalysis>true</DisablePackageVulnerabilityAnalysis>
|
||||||
|
<NoWarn>$(NoWarn);NU1900</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
<ProjectReference Include="..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.FileCache;
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.WebAPI.Files;
|
using MareSynchronos.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -10,21 +11,23 @@ public class FileDownloadManagerFactory
|
|||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||||
|
private readonly MareConfigService _mareConfigService;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly MareMediator _mareMediator;
|
private readonly MareMediator _mareMediator;
|
||||||
|
|
||||||
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
||||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mareMediator = mareMediator;
|
_mareMediator = mareMediator;
|
||||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileDownloadManager Create()
|
public FileDownloadManager Create()
|
||||||
{
|
{
|
||||||
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor, _mareConfigService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,57 @@
|
|||||||
using MareSynchronos.API.Data;
|
using MareSynchronos.FileCache;
|
||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.Interop.Ipc;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MareSynchronos.PlayerData.Factories;
|
namespace MareSynchronos.PlayerData.Factories;
|
||||||
|
|
||||||
public class PairFactory
|
public class PairHandlerFactory
|
||||||
{
|
{
|
||||||
private readonly PairHandlerFactory _cachedPlayerFactory;
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
private readonly FileDownloadManagerFactory _fileDownloadManagerFactory;
|
||||||
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
|
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly MareMediator _mareMediator;
|
private readonly MareMediator _mareMediator;
|
||||||
private readonly MareConfigService _mareConfig;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
|
private readonly PairAnalyzerFactory _pairAnalyzerFactory;
|
||||||
|
private readonly VisibilityService _visibilityService;
|
||||||
|
|
||||||
public PairFactory(ILoggerFactory loggerFactory, PairHandlerFactory cachedPlayerFactory,
|
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
|
||||||
MareMediator mareMediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager)
|
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||||
|
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||||
|
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
|
||||||
|
PairAnalyzerFactory pairAnalyzerFactory,
|
||||||
|
MareConfigService configService, VisibilityService visibilityService)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_cachedPlayerFactory = cachedPlayerFactory;
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
|
_ipcManager = ipcManager;
|
||||||
|
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
|
_hostApplicationLifetime = hostApplicationLifetime;
|
||||||
|
_fileCacheManager = fileCacheManager;
|
||||||
_mareMediator = mareMediator;
|
_mareMediator = mareMediator;
|
||||||
_mareConfig = mareConfig;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_pairAnalyzerFactory = pairAnalyzerFactory;
|
||||||
|
_configService = configService;
|
||||||
|
_visibilityService = visibilityService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pair Create(UserData userData)
|
public PairHandler Create(Pair pair)
|
||||||
{
|
{
|
||||||
return new Pair(_loggerFactory.CreateLogger<Pair>(), userData, _cachedPlayerFactory, _mareMediator, _mareConfig, _serverConfigurationManager);
|
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory,
|
||||||
|
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||||
|
_fileCacheManager, _mareMediator, _playerPerformanceService, _configService, _visibilityService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using MareSynchronos.PlayerData.Pairs;
|
|||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Events;
|
using MareSynchronos.Services.Events;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
|
||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using MareSynchronos.WebAPI.Files;
|
using MareSynchronos.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -29,7 +28,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
|
||||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
private readonly VisibilityService _visibilityService;
|
private readonly VisibilityService _visibilityService;
|
||||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||||
@@ -53,7 +51,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileDbManager, MareMediator mediator,
|
FileCacheManager fileDbManager, MareMediator mediator,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
ServerConfigurationManager serverConfigManager,
|
|
||||||
MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator)
|
MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
Pair = pair;
|
Pair = pair;
|
||||||
@@ -65,7 +62,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_fileDbManager = fileDbManager;
|
_fileDbManager = fileDbManager;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
_serverConfigManager = serverConfigManager;
|
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_visibilityService = visibilityService;
|
_visibilityService = visibilityService;
|
||||||
|
|
||||||
@@ -887,4 +883,4 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count);
|
Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count);
|
||||||
return [.. missingFiles];
|
return [.. missingFiles];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,6 +270,20 @@ public class Pair : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_applicationCts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
_applicationCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
public void SetNote(string note)
|
public void SetNote(string note)
|
||||||
{
|
{
|
||||||
_serverConfigurationManager.SetNoteForUid(UserData.UID, note);
|
_serverConfigurationManager.SetNoteForUid(UserData.UID, note);
|
||||||
@@ -368,4 +382,4 @@ public class Pair : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddGroupPair(GroupPairFullInfoDto dto)
|
public void AddGroupPair(GroupPairFullInfoDto dto, bool isInitialLoad = false)
|
||||||
{
|
{
|
||||||
if (!_allClientPairs.ContainsKey(dto.User))
|
if (!_allClientPairs.ContainsKey(dto.User))
|
||||||
_allClientPairs[dto.User] = _pairFactory.Create(dto.User);
|
_allClientPairs[dto.User] = _pairFactory.Create(dto.User);
|
||||||
@@ -59,6 +59,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
var group = _allGroups[dto.Group];
|
var group = _allGroups[dto.Group];
|
||||||
_allClientPairs[dto.User].GroupPair[group] = dto;
|
_allClientPairs[dto.User].GroupPair[group] = dto;
|
||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
|
|
||||||
|
if (!isInitialLoad)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ApplyDefaultGroupPermissionsMessage(dto));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pair? GetPairByUID(string uid)
|
public Pair? GetPairByUID(string uid)
|
||||||
@@ -88,6 +93,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
LastAddedUser = _allClientPairs[dto.User];
|
LastAddedUser = _allClientPairs[dto.User];
|
||||||
_allClientPairs[dto.User].ApplyLastReceivedData();
|
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
|
|
||||||
|
if (addToLastAddedUser)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ApplyDefaultPairPermissionsMessage(dto));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearPairs()
|
public void ClearPairs()
|
||||||
@@ -210,9 +220,16 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void SetGroupInfo(GroupInfoDto dto)
|
public void SetGroupInfo(GroupInfoDto dto)
|
||||||
{
|
{
|
||||||
_allGroups[dto.Group].Group = dto.Group;
|
if (!_allGroups.TryGetValue(dto.Group, out var groupInfo))
|
||||||
_allGroups[dto.Group].Owner = dto.Owner;
|
{
|
||||||
_allGroups[dto.Group].GroupPermissions = dto.GroupPermissions;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupInfo.Group = dto.Group;
|
||||||
|
groupInfo.Owner = dto.Owner;
|
||||||
|
groupInfo.GroupPermissions = dto.GroupPermissions;
|
||||||
|
groupInfo.IsTemporary = dto.IsTemporary;
|
||||||
|
groupInfo.ExpiresAt = dto.ExpiresAt;
|
||||||
|
|
||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
}
|
}
|
||||||
@@ -400,4 +417,4 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
_directPairsInternal = DirectPairsLazy();
|
_directPairsInternal = DirectPairsLazy();
|
||||||
_groupPairsInternal = GroupPairsLazy();
|
_groupPairsInternal = GroupPairsLazy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using MareSynchronos.Services;
|
|||||||
using MareSynchronos.Services.Events;
|
using MareSynchronos.Services.Events;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.Services.Notifications;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
using MareSynchronos.UI.Components;
|
using MareSynchronos.UI.Components;
|
||||||
using MareSynchronos.UI.Components.Popup;
|
using MareSynchronos.UI.Components.Popup;
|
||||||
@@ -29,7 +30,7 @@ using MareSynchronos.Services.CharaData;
|
|||||||
|
|
||||||
using MareSynchronos;
|
using MareSynchronos;
|
||||||
|
|
||||||
namespace Snowcloak;
|
namespace Umbra;
|
||||||
|
|
||||||
public sealed class Plugin : IDalamudPlugin
|
public sealed class Plugin : IDalamudPlugin
|
||||||
{
|
{
|
||||||
@@ -39,8 +40,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
public static Plugin Self;
|
public static Plugin Self;
|
||||||
#pragma warning restore CA2211, CS8618, MA0069, S1104, S2223
|
#pragma warning restore CA2211, CS8618, MA0069, S1104, S2223
|
||||||
public Action<IFramework>? RealOnFrameworkUpdate { get; set; }
|
public Action<IFramework>? RealOnFrameworkUpdate { get; set; }
|
||||||
|
|
||||||
// Proxy function in the SnowcloakSync namespace to avoid confusion in /xlstats
|
|
||||||
public void OnFrameworkUpdate(IFramework framework)
|
public void OnFrameworkUpdate(IFramework framework)
|
||||||
{
|
{
|
||||||
RealOnFrameworkUpdate?.Invoke(framework);
|
RealOnFrameworkUpdate?.Invoke(framework);
|
||||||
@@ -98,6 +97,13 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<HubFactory>();
|
collection.AddSingleton<HubFactory>();
|
||||||
collection.AddSingleton<FileUploadManager>();
|
collection.AddSingleton<FileUploadManager>();
|
||||||
collection.AddSingleton<FileTransferOrchestrator>();
|
collection.AddSingleton<FileTransferOrchestrator>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.DiscoveryConfigProvider>();
|
||||||
|
collection.AddSingleton<MareSynchronos.WebAPI.AutoDetect.DiscoveryApiClient>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectRequestService>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.SyncshellDiscoveryService>();
|
||||||
collection.AddSingleton<MarePlugin>();
|
collection.AddSingleton<MarePlugin>();
|
||||||
collection.AddSingleton<MareProfileManager>();
|
collection.AddSingleton<MareProfileManager>();
|
||||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||||
@@ -112,6 +118,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<PluginWarningNotificationService>();
|
collection.AddSingleton<PluginWarningNotificationService>();
|
||||||
collection.AddSingleton<FileCompactor>();
|
collection.AddSingleton<FileCompactor>();
|
||||||
collection.AddSingleton<TagHandler>();
|
collection.AddSingleton<TagHandler>();
|
||||||
|
collection.AddSingleton<SyncDefaultsService>();
|
||||||
collection.AddSingleton<UidDisplayHandler>();
|
collection.AddSingleton<UidDisplayHandler>();
|
||||||
collection.AddSingleton<PluginWatcherService>();
|
collection.AddSingleton<PluginWatcherService>();
|
||||||
collection.AddSingleton<PlayerPerformanceService>();
|
collection.AddSingleton<PlayerPerformanceService>();
|
||||||
@@ -121,6 +128,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<CharaDataCharacterHandler>();
|
collection.AddSingleton<CharaDataCharacterHandler>();
|
||||||
collection.AddSingleton<CharaDataNearbyManager>();
|
collection.AddSingleton<CharaDataNearbyManager>();
|
||||||
collection.AddSingleton<CharaDataGposeTogetherManager>();
|
collection.AddSingleton<CharaDataGposeTogetherManager>();
|
||||||
|
collection.AddSingleton<McdfShareManager>();
|
||||||
|
|
||||||
collection.AddSingleton<VfxSpawnManager>();
|
collection.AddSingleton<VfxSpawnManager>();
|
||||||
collection.AddSingleton<BlockedCharacterHandler>();
|
collection.AddSingleton<BlockedCharacterHandler>();
|
||||||
@@ -142,6 +150,11 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<IpcCallerMare>();
|
collection.AddSingleton<IpcCallerMare>();
|
||||||
collection.AddSingleton<IpcManager>();
|
collection.AddSingleton<IpcManager>();
|
||||||
collection.AddSingleton<NotificationService>();
|
collection.AddSingleton<NotificationService>();
|
||||||
|
collection.AddSingleton<TemporarySyncshellNotificationService>();
|
||||||
|
collection.AddSingleton<PartyListTypingService>();
|
||||||
|
collection.AddSingleton<TypingIndicatorStateService>();
|
||||||
|
collection.AddSingleton<ChatTwoCompatibilityService>();
|
||||||
|
collection.AddSingleton<NotificationTracker>();
|
||||||
|
|
||||||
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
@@ -154,6 +167,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new RemoteConfigCacheService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new RemoteConfigCacheService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton((s) => new NotificationsConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<MareConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<MareConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
||||||
@@ -165,6 +179,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerBlockConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerBlockConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<RemoteConfigCacheService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<RemoteConfigCacheService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotificationsConfigService>());
|
||||||
collection.AddSingleton<ConfigurationMigrator>();
|
collection.AddSingleton<ConfigurationMigrator>();
|
||||||
collection.AddSingleton<ConfigurationSaveService>();
|
collection.AddSingleton<ConfigurationSaveService>();
|
||||||
|
|
||||||
@@ -173,16 +188,25 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
// add scoped services
|
// add scoped services
|
||||||
collection.AddScoped<CacheMonitor>();
|
collection.AddScoped<CacheMonitor>();
|
||||||
collection.AddScoped<UiFactory>();
|
collection.AddScoped<UiFactory>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, SettingsUi>();
|
collection.AddScoped<SettingsUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
|
collection.AddScoped<CompactUi>();
|
||||||
|
collection.AddScoped<EditProfileUi>();
|
||||||
|
collection.AddScoped<DataAnalysisUi>();
|
||||||
|
collection.AddScoped<CharaDataHubUi>();
|
||||||
|
collection.AddScoped<AutoDetectUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<SettingsUi>());
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<CompactUi>());
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<AutoDetectUi>());
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, ChangelogUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<DataAnalysisUi>());
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<EditProfileUi>());
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, TypingIndicatorOverlay>();
|
||||||
collection.AddScoped<IPopupHandler, ReportPopupHandler>();
|
collection.AddScoped<IPopupHandler, ReportPopupHandler>();
|
||||||
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||||
collection.AddScoped<CacheCreationService>();
|
collection.AddScoped<CacheCreationService>();
|
||||||
@@ -194,11 +218,13 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddScoped<UiSharedService>();
|
collection.AddScoped<UiSharedService>();
|
||||||
collection.AddScoped<ChatService>();
|
collection.AddScoped<ChatService>();
|
||||||
collection.AddScoped<GuiHookService>();
|
collection.AddScoped<GuiHookService>();
|
||||||
|
collection.AddScoped<ChatTypingDetectionService>();
|
||||||
|
|
||||||
collection.AddHostedService(p => p.GetRequiredService<PluginWatcherService>());
|
collection.AddHostedService(p => p.GetRequiredService<PluginWatcherService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
|
collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
|
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<TemporarySyncshellNotificationService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
|
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
|
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
|
||||||
@@ -207,9 +233,23 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
|
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
|
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.SyncshellDiscoveryService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<ChatTwoCompatibilityService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>());
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var partyListTypingService = _host.Services.GetRequiredService<PartyListTypingService>();
|
||||||
|
pluginInterface.UiBuilder.Draw += partyListTypingService.Draw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
pluginLog.Warning(e, "Failed to initialize PartyListTypingService draw hook");
|
||||||
|
}
|
||||||
|
|
||||||
_ = Task.Run(async () => {
|
_ = Task.Run(async () => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -227,4 +267,4 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
_host.StopAsync().GetAwaiter().GetResult();
|
_host.StopAsync().GetAwaiter().GetResult();
|
||||||
_host.Dispose();
|
_host.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
386
MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs
Normal file
386
MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.WebAPI.AutoDetect;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public class AutoDetectRequestService
|
||||||
|
{
|
||||||
|
private readonly ILogger<AutoDetectRequestService> _logger;
|
||||||
|
private readonly DiscoveryConfigProvider _configProvider;
|
||||||
|
private readonly DiscoveryApiClient _client;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly DalamudUtilService _dalamud;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly object _syncRoot = new();
|
||||||
|
private readonly Dictionary<string, DateTime> _activeCooldowns = new(StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<string, RefusalTracker> _refusalTrackers = new(StringComparer.Ordinal);
|
||||||
|
private readonly ConcurrentDictionary<string, PendingRequestInfo> _pendingRequests = new(StringComparer.Ordinal);
|
||||||
|
private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5);
|
||||||
|
private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15);
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
|
||||||
|
public AutoDetectRequestService(ILogger<AutoDetectRequestService> logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configProvider = configProvider;
|
||||||
|
_client = client;
|
||||||
|
_configService = configService;
|
||||||
|
_mediator = mediator;
|
||||||
|
_dalamud = dalamudUtilService;
|
||||||
|
_apiController = apiController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendRequestAsync(string? token, string? uid = null, string? targetDisplayName = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!_configService.Current.AllowAutoDetectPairRequests)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Nearby request blocked: AllowAutoDetectPairRequests is disabled");
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(uid))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Nearby request blocked: no token or UID provided");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetKey = BuildTargetKey(uid, token, targetDisplayName);
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now)
|
||||||
|
{
|
||||||
|
PublishLockNotification(tracker.LockUntil.Value - now);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
tracker.Count = 0;
|
||||||
|
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||||
|
{
|
||||||
|
_refusalTrackers.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activeCooldowns.TryGetValue(targetKey, out var lastSent))
|
||||||
|
{
|
||||||
|
var elapsed = now - lastSent;
|
||||||
|
if (elapsed < RequestCooldown)
|
||||||
|
{
|
||||||
|
PublishCooldownNotification(RequestCooldown - elapsed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed >= RequestCooldown)
|
||||||
|
{
|
||||||
|
_activeCooldowns.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var endpoint = _configProvider.RequestEndpoint;
|
||||||
|
if (string.IsNullOrEmpty(endpoint))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No request endpoint configured");
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request failed", "Server does not expose request endpoint.", NotificationType.Error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string? displayName = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
displayName = me?.Name.TextValue;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
var requestToken = string.IsNullOrEmpty(token) ? null : token;
|
||||||
|
var requestUid = requestToken == null ? uid : null;
|
||||||
|
|
||||||
|
_logger.LogInformation("Nearby: sending pair request via {endpoint}", endpoint);
|
||||||
|
var ok = await _client.SendRequestAsync(endpoint!, requestToken, requestUid, displayName, ct).ConfigureAwait(false);
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
_activeCooldowns[targetKey] = DateTime.UtcNow;
|
||||||
|
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker.Count = 0;
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||||
|
{
|
||||||
|
_refusalTrackers.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
|
||||||
|
var pendingKey = EnsureTargetKey(targetKey);
|
||||||
|
var label = !string.IsNullOrWhiteSpace(targetDisplayName)
|
||||||
|
? targetDisplayName!
|
||||||
|
: (!string.IsNullOrEmpty(uid) ? uid : (!string.IsNullOrEmpty(token) ? token : pendingKey));
|
||||||
|
_pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, token, label, DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
_activeCooldowns.Remove(targetKey);
|
||||||
|
if (!_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker = new RefusalTracker();
|
||||||
|
_refusalTrackers[targetKey] = tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
tracker.Count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.Count++;
|
||||||
|
if (tracker.Count >= 3)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = now.Add(RefusalLockDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_pendingRequests.TryRemove(targetKey, out _);
|
||||||
|
}
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendAcceptNotifyAsync(string targetUid, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var endpoint = _configProvider.AcceptEndpoint;
|
||||||
|
if (string.IsNullOrEmpty(endpoint))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No accept endpoint configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string? displayName = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
displayName = me?.Name.TextValue;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
_logger.LogInformation("Nearby: sending accept notify via {endpoint}", endpoint);
|
||||||
|
return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendDirectUidRequestAsync(string uid, string? targetDisplayName = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!_configService.Current.AllowAutoDetectPairRequests)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Nearby request blocked: AllowAutoDetectPairRequests is disabled");
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(uid))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Direct pair request aborted: UID is empty");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetKey = BuildTargetKey(uid, null, targetDisplayName);
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now)
|
||||||
|
{
|
||||||
|
PublishLockNotification(tracker.LockUntil.Value - now);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
tracker.Count = 0;
|
||||||
|
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||||
|
{
|
||||||
|
_refusalTrackers.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activeCooldowns.TryGetValue(targetKey, out var lastSent))
|
||||||
|
{
|
||||||
|
var elapsed = now - lastSent;
|
||||||
|
if (elapsed < RequestCooldown)
|
||||||
|
{
|
||||||
|
PublishCooldownNotification(RequestCooldown - elapsed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed >= RequestCooldown)
|
||||||
|
{
|
||||||
|
_activeCooldowns.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _apiController.UserAddPair(new UserDto(new UserData(uid))).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
_activeCooldowns[targetKey] = DateTime.UtcNow;
|
||||||
|
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker.Count = 0;
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||||
|
{
|
||||||
|
_refusalTrackers.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
|
||||||
|
var pendingKey = EnsureTargetKey(targetKey);
|
||||||
|
var label = !string.IsNullOrWhiteSpace(targetDisplayName) ? targetDisplayName! : uid;
|
||||||
|
_pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, null, label, DateTime.UtcNow);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Direct pair request failed for {uid}", uid);
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
_activeCooldowns.Remove(targetKey);
|
||||||
|
if (!_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker = new RefusalTracker();
|
||||||
|
_refusalTrackers[targetKey] = tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
tracker.Count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.Count++;
|
||||||
|
if (tracker.Count >= 3)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = now.Add(RefusalLockDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_pendingRequests.TryRemove(targetKey, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? BuildTargetKey(string? uid, string? token, string? displayName)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(uid)) return "uid:" + uid;
|
||||||
|
if (!string.IsNullOrEmpty(token)) return "token:" + token;
|
||||||
|
if (!string.IsNullOrEmpty(displayName)) return "name:" + displayName;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishCooldownNotification(TimeSpan remaining)
|
||||||
|
{
|
||||||
|
var durationText = FormatDuration(remaining);
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request en attente", $"Nearby request déjà envoyée. Merci d'attendre environ {durationText} avant de réessayer.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishLockNotification(TimeSpan remaining)
|
||||||
|
{
|
||||||
|
var durationText = FormatDuration(remaining);
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request bloquée", $"Nearby request bloquée après plusieurs refus. Réessayez dans {durationText}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatDuration(TimeSpan remaining)
|
||||||
|
{
|
||||||
|
if (remaining.TotalMinutes >= 1)
|
||||||
|
{
|
||||||
|
var minutes = Math.Max(1, (int)Math.Ceiling(remaining.TotalMinutes));
|
||||||
|
return minutes == 1 ? "1 minute" : minutes + " minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds));
|
||||||
|
return seconds == 1 ? "1 seconde" : seconds + " secondes";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RefusalTracker
|
||||||
|
{
|
||||||
|
public int Count;
|
||||||
|
public DateTime? LockUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<PendingRequestInfo> GetPendingRequestsSnapshot()
|
||||||
|
{
|
||||||
|
return _pendingRequests.Values.OrderByDescending(v => v.SentAt).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemovePendingRequestByUid(string uid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(uid)) return;
|
||||||
|
foreach (var kvp in _pendingRequests)
|
||||||
|
{
|
||||||
|
if (string.Equals(kvp.Value.Uid, uid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_pendingRequests.TryRemove(kvp.Key, out _);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemovePendingRequestByKey(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key)) return;
|
||||||
|
_pendingRequests.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EnsureTargetKey(string? targetKey)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(targetKey) ? targetKey! : "target:" + Guid.NewGuid().ToString("N");
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PendingRequestInfo(string Key, string? Uid, string? Token, string TargetDisplayName, DateTime SentAt);
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public sealed class AutoDetectSuppressionService : IHostedService, IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private static readonly string[] ContentTypeKeywords =
|
||||||
|
[
|
||||||
|
"dungeon",
|
||||||
|
"donjon",
|
||||||
|
"raid",
|
||||||
|
"trial",
|
||||||
|
"défi",
|
||||||
|
"front",
|
||||||
|
"frontline",
|
||||||
|
"pvp",
|
||||||
|
"jcj",
|
||||||
|
"conflict",
|
||||||
|
"conflit"
|
||||||
|
];
|
||||||
|
|
||||||
|
private readonly ILogger<AutoDetectSuppressionService> _logger;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
|
private readonly IDataManager _dataManager;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
|
||||||
|
private bool _isSuppressed;
|
||||||
|
private bool _hasSavedState;
|
||||||
|
private bool _savedDiscoveryEnabled;
|
||||||
|
private bool _savedAllowRequests;
|
||||||
|
private bool _suppressionWarningShown;
|
||||||
|
public bool IsSuppressed => _isSuppressed;
|
||||||
|
|
||||||
|
public AutoDetectSuppressionService(ILogger<AutoDetectSuppressionService> logger,
|
||||||
|
MareConfigService configService, IClientState clientState,
|
||||||
|
IDataManager dataManager, DalamudUtilService dalamudUtilService, MareMediator mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configService = configService;
|
||||||
|
_clientState = clientState;
|
||||||
|
_dataManager = dataManager;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator => _mediator;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => UpdateSuppressionState());
|
||||||
|
_mediator.Subscribe<DalamudLoginMessage>(this, _ => UpdateSuppressionState());
|
||||||
|
_mediator.Subscribe<DalamudLogoutMessage>(this, _ => ClearSuppression());
|
||||||
|
UpdateSuppressionState();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.UnsubscribeAll(this);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSuppressionState()
|
||||||
|
{
|
||||||
|
_ = _dalamudUtilService.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
|
||||||
|
{
|
||||||
|
ClearSuppression();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint territoryId = _clientState.TerritoryType;
|
||||||
|
bool shouldSuppress = ShouldSuppressForTerritory(territoryId);
|
||||||
|
ApplySuppression(shouldSuppress, territoryId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to update AutoDetect suppression state");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySuppression(bool shouldSuppress, uint territoryId)
|
||||||
|
{
|
||||||
|
if (shouldSuppress)
|
||||||
|
{
|
||||||
|
if (!_isSuppressed)
|
||||||
|
{
|
||||||
|
_savedDiscoveryEnabled = _configService.Current.EnableAutoDetectDiscovery;
|
||||||
|
_savedAllowRequests = _configService.Current.AllowAutoDetectPairRequests;
|
||||||
|
_hasSavedState = true;
|
||||||
|
_isSuppressed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
if (_configService.Current.EnableAutoDetectDiscovery)
|
||||||
|
{
|
||||||
|
_configService.Current.EnableAutoDetectDiscovery = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (_configService.Current.AllowAutoDetectPairRequests)
|
||||||
|
{
|
||||||
|
_configService.Current.AllowAutoDetectPairRequests = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("AutoDetect temporarily disabled in instanced content (territory {territoryId}).", territoryId);
|
||||||
|
if (!_suppressionWarningShown)
|
||||||
|
{
|
||||||
|
_suppressionWarningShown = true;
|
||||||
|
const string warningText = "Zone instanciée détectée : les fonctions AutoDetect/Nearby sont coupées pour économiser de la bande passante.";
|
||||||
|
_mediator.Publish(new DualNotificationMessage("AutoDetect désactivé",
|
||||||
|
warningText,
|
||||||
|
NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_isSuppressed) return;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
bool wasSuppressed = _suppressionWarningShown;
|
||||||
|
if (_hasSavedState)
|
||||||
|
{
|
||||||
|
if (_configService.Current.EnableAutoDetectDiscovery != _savedDiscoveryEnabled)
|
||||||
|
{
|
||||||
|
_configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (_configService.Current.AllowAutoDetectPairRequests != _savedAllowRequests)
|
||||||
|
{
|
||||||
|
_configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isSuppressed = false;
|
||||||
|
_hasSavedState = false;
|
||||||
|
_suppressionWarningShown = false;
|
||||||
|
|
||||||
|
if (changed || wasSuppressed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("AutoDetect restored after leaving instanced content (territory {territoryId}).", territoryId);
|
||||||
|
const string restoredText = "Vous avez quitté la zone instanciée : AutoDetect/Nearby fonctionnent de nouveau.";
|
||||||
|
_mediator.Publish(new DualNotificationMessage("AutoDetect réactivé",
|
||||||
|
restoredText,
|
||||||
|
NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearSuppression()
|
||||||
|
{
|
||||||
|
if (!_isSuppressed) return;
|
||||||
|
_isSuppressed = false;
|
||||||
|
if (_hasSavedState)
|
||||||
|
{
|
||||||
|
_configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled;
|
||||||
|
_configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests;
|
||||||
|
}
|
||||||
|
_hasSavedState = false;
|
||||||
|
_suppressionWarningShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldSuppressForTerritory(uint territoryId)
|
||||||
|
{
|
||||||
|
if (territoryId == 0) return false;
|
||||||
|
|
||||||
|
var cfcSheet = _dataManager.GetExcelSheet<ContentFinderCondition>();
|
||||||
|
if (cfcSheet == null) return false;
|
||||||
|
|
||||||
|
var cfc = cfcSheet.FirstOrDefault(c => c.TerritoryType.RowId == territoryId);
|
||||||
|
if (cfc.RowId == 0) return false;
|
||||||
|
|
||||||
|
if (MatchesSuppressionKeyword(cfc.Name.ToString())) return true;
|
||||||
|
|
||||||
|
var contentType = cfc.ContentType.Value;
|
||||||
|
if (contentType.RowId == 0) return false;
|
||||||
|
|
||||||
|
return MatchesSuppressionKeyword(contentType.Name.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesSuppressionKeyword(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||||
|
return ContentTypeKeywords.Any(keyword => text.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
170
MareSynchronos/Services/AutoDetect/DiscoveryConfigProvider.cs
Normal file
170
MareSynchronos/Services/AutoDetect/DiscoveryConfigProvider.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.WebAPI.SignalR;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public class DiscoveryConfigProvider
|
||||||
|
{
|
||||||
|
private readonly ILogger<DiscoveryConfigProvider> _logger;
|
||||||
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
|
private readonly TokenProvider _tokenProvider;
|
||||||
|
|
||||||
|
private WellKnownRoot? _config;
|
||||||
|
|
||||||
|
public DiscoveryConfigProvider(ILogger<DiscoveryConfigProvider> logger, ServerConfigurationManager serverManager, TokenProvider tokenProvider)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_serverManager = serverManager;
|
||||||
|
_tokenProvider = tokenProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasConfig => _config != null;
|
||||||
|
public bool NearbyEnabled => _config?.NearbyDiscovery?.Enabled ?? false;
|
||||||
|
public byte[]? Salt => _config?.NearbyDiscovery?.SaltBytes;
|
||||||
|
public string? SaltB64 => _config?.NearbyDiscovery?.SaltB64;
|
||||||
|
public DateTimeOffset? SaltExpiresAt => _config?.NearbyDiscovery?.SaltExpiresAt;
|
||||||
|
public int RefreshSec => _config?.NearbyDiscovery?.RefreshSec ?? 300;
|
||||||
|
public int MinQueryIntervalMs => _config?.NearbyDiscovery?.Policies?.MinQueryIntervalMs ?? 2000;
|
||||||
|
public int MaxQueryBatch => _config?.NearbyDiscovery?.Policies?.MaxQueryBatch ?? 100;
|
||||||
|
public string? PublishEndpoint => _config?.NearbyDiscovery?.Endpoints?.Publish;
|
||||||
|
public string? QueryEndpoint => _config?.NearbyDiscovery?.Endpoints?.Query;
|
||||||
|
public string? RequestEndpoint => _config?.NearbyDiscovery?.Endpoints?.Request;
|
||||||
|
public string? AcceptEndpoint => _config?.NearbyDiscovery?.Endpoints?.Accept;
|
||||||
|
|
||||||
|
public bool TryLoadFromStapled()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = _tokenProvider.GetStapledWellKnown(_serverManager.CurrentApiUrl);
|
||||||
|
if (string.IsNullOrEmpty(json)) return false;
|
||||||
|
|
||||||
|
var root = JsonSerializer.Deserialize<WellKnownRoot>(json!);
|
||||||
|
if (root == null) return false;
|
||||||
|
|
||||||
|
root.NearbyDiscovery?.Hydrate();
|
||||||
|
_config = root;
|
||||||
|
_logger.LogDebug("Loaded Nearby well-known (stapled), enabled={enabled}, expires={exp}", NearbyEnabled, _config?.NearbyDiscovery?.SaltExpiresAt);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to parse stapled well-known");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> TryFetchFromServerAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var baseUrl = _serverManager.CurrentApiUrl
|
||||||
|
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase);
|
||||||
|
// Try likely candidates based on nginx config
|
||||||
|
string[] candidates =
|
||||||
|
[
|
||||||
|
"/.well-known/Umbra/client", // matches provided nginx
|
||||||
|
"/.well-known/umbra", // lowercase variant
|
||||||
|
];
|
||||||
|
|
||||||
|
using var http = new HttpClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ver = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", $"{ver.Major}.{ver.Minor}.{ver.Build}"));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
foreach (var path in candidates)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uri = new Uri(new Uri(baseUrl), path);
|
||||||
|
var json = await http.GetStringAsync(uri, ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(json)) continue;
|
||||||
|
|
||||||
|
var root = JsonSerializer.Deserialize<WellKnownRoot>(json);
|
||||||
|
if (root == null) continue;
|
||||||
|
|
||||||
|
root.NearbyDiscovery?.Hydrate();
|
||||||
|
_config = root;
|
||||||
|
_logger.LogInformation("Loaded Nearby well-known (http {path}), enabled={enabled}", path, NearbyEnabled);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Nearby well-known fetch failed for {path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Nearby well-known not found via HTTP candidates");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to fetch Nearby well-known via HTTP");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsExpired()
|
||||||
|
{
|
||||||
|
if (_config?.NearbyDiscovery?.SaltExpiresAt == null) return false;
|
||||||
|
return DateTimeOffset.UtcNow > _config.NearbyDiscovery.SaltExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTOs for well-known JSON
|
||||||
|
private sealed class WellKnownRoot
|
||||||
|
{
|
||||||
|
[JsonPropertyName("features")] public Features? Features { get; set; }
|
||||||
|
[JsonPropertyName("nearby_discovery")] public Nearby? NearbyDiscovery { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Features
|
||||||
|
{
|
||||||
|
[JsonPropertyName("nearby_discovery")] public bool NearbyDiscovery { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Nearby
|
||||||
|
{
|
||||||
|
[JsonPropertyName("enabled")] public bool Enabled { get; set; }
|
||||||
|
[JsonPropertyName("hash_algo")] public string? HashAlgo { get; set; }
|
||||||
|
[JsonPropertyName("salt_b64")] public string? SaltB64 { get; set; }
|
||||||
|
[JsonPropertyName("salt_expires_at")] public string? SaltExpiresAtRaw { get; set; }
|
||||||
|
[JsonPropertyName("refresh_sec")] public int RefreshSec { get; set; } = 300;
|
||||||
|
[JsonPropertyName("endpoints")] public Endpoints? Endpoints { get; set; }
|
||||||
|
[JsonPropertyName("policies")] public Policies? Policies { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] public byte[]? SaltBytes { get; private set; }
|
||||||
|
[JsonIgnore] public DateTimeOffset? SaltExpiresAt { get; private set; }
|
||||||
|
|
||||||
|
public void Hydrate()
|
||||||
|
{
|
||||||
|
try { SaltBytes = string.IsNullOrEmpty(SaltB64) ? null : Convert.FromBase64String(SaltB64!); } catch { SaltBytes = null; }
|
||||||
|
if (DateTimeOffset.TryParse(SaltExpiresAtRaw, out var dto)) SaltExpiresAt = dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Endpoints
|
||||||
|
{
|
||||||
|
[JsonPropertyName("publish")] public string? Publish { get; set; }
|
||||||
|
[JsonPropertyName("query")] public string? Query { get; set; }
|
||||||
|
[JsonPropertyName("request")] public string? Request { get; set; }
|
||||||
|
[JsonPropertyName("accept")] public string? Accept { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Policies
|
||||||
|
{
|
||||||
|
[JsonPropertyName("max_query_batch")] public int MaxQueryBatch { get; set; } = 100;
|
||||||
|
[JsonPropertyName("min_query_interval_ms")] public int MinQueryIntervalMs { get; set; } = 2000;
|
||||||
|
[JsonPropertyName("rate_limit_per_min")] public int RateLimitPerMin { get; set; } = 30;
|
||||||
|
[JsonPropertyName("token_ttl_sec")] public int TokenTtlSec { get; set; } = 120;
|
||||||
|
}
|
||||||
|
}
|
||||||
514
MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs
Normal file
514
MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.WebAPI.AutoDetect;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly ILogger<NearbyDiscoveryService> _logger;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly MareConfigService _config;
|
||||||
|
private readonly DiscoveryConfigProvider _configProvider;
|
||||||
|
private readonly DalamudUtilService _dalamud;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
|
private readonly DiscoveryApiClient _api;
|
||||||
|
private CancellationTokenSource? _loopCts;
|
||||||
|
private string? _lastPublishedSignature;
|
||||||
|
private bool _loggedLocalOnly;
|
||||||
|
private int _lastLocalCount = -1;
|
||||||
|
private int _lastMatchCount = -1;
|
||||||
|
private bool _loggedConfigReady;
|
||||||
|
private string? _lastSnapshotSig;
|
||||||
|
private volatile bool _isConnected;
|
||||||
|
private bool _notifiedDisabled;
|
||||||
|
private bool _notifiedEnabled;
|
||||||
|
private bool _disableSent;
|
||||||
|
private bool _lastAutoDetectState;
|
||||||
|
private DateTime _lastHeartbeat = DateTime.MinValue;
|
||||||
|
private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(75);
|
||||||
|
private readonly object _entriesLock = new();
|
||||||
|
private List<NearbyEntry> _lastEntries = [];
|
||||||
|
|
||||||
|
public NearbyDiscoveryService(ILogger<NearbyDiscoveryService> logger, MareMediator mediator,
|
||||||
|
MareConfigService config, DiscoveryConfigProvider configProvider, DalamudUtilService dalamudUtilService,
|
||||||
|
IObjectTable objectTable, DiscoveryApiClient api)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
_config = config;
|
||||||
|
_configProvider = configProvider;
|
||||||
|
_dalamud = dalamudUtilService;
|
||||||
|
_objectTable = objectTable;
|
||||||
|
_api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator => _mediator;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref _loopCts);
|
||||||
|
_loopCts = new CancellationTokenSource();
|
||||||
|
_mediator.Subscribe<ConnectedMessage>(this, _ => { _isConnected = true; _configProvider.TryLoadFromStapled(); });
|
||||||
|
_mediator.Subscribe<DisconnectedMessage>(this, _ => { _isConnected = false; _lastPublishedSignature = null; });
|
||||||
|
_mediator.Subscribe<AllowPairRequestsToggled>(this, OnAllowPairRequestsToggled);
|
||||||
|
_ = Task.Run(() => Loop(_loopCts.Token));
|
||||||
|
_lastAutoDetectState = _config.Current.EnableAutoDetectDiscovery;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
private async void OnAllowPairRequestsToggled(AllowPairRequestsToggled msg)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_config.Current.EnableAutoDetectDiscovery) return;
|
||||||
|
// Force a publish now so the server immediately reflects the new allow/deny state
|
||||||
|
_lastPublishedSignature = null; // ensure next loop won't skip
|
||||||
|
await PublishSelfOnceAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "OnAllowPairRequestsToggled failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PublishSelfOnceAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_config.Current.EnableAutoDetectDiscovery || !_dalamud.IsLoggedIn || !_isConnected) return;
|
||||||
|
|
||||||
|
if (!_configProvider.HasConfig || _configProvider.IsExpired())
|
||||||
|
{
|
||||||
|
if (!_configProvider.TryLoadFromStapled())
|
||||||
|
{
|
||||||
|
await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ep = _configProvider.PublishEndpoint;
|
||||||
|
var saltBytes = _configProvider.Salt;
|
||||||
|
if (string.IsNullOrEmpty(ep) || saltBytes is not { Length: > 0 }) return;
|
||||||
|
|
||||||
|
var saltHex = Convert.ToHexString(saltBytes);
|
||||||
|
string? displayName = null;
|
||||||
|
ushort meWorld = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
if (me != null)
|
||||||
|
{
|
||||||
|
displayName = me.Name.TextValue;
|
||||||
|
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
|
||||||
|
meWorld = (ushort)mePc.HomeWorld.RowId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(displayName)) return;
|
||||||
|
|
||||||
|
var selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
|
||||||
|
var ok = await _api.PublishAsync(ep!, new[] { selfHash }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
|
||||||
|
_logger.LogInformation("Nearby publish (manual/immediate): {result}", ok ? "success" : "failed");
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_lastPublishedSignature = selfHash;
|
||||||
|
_lastHeartbeat = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Immediate publish failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.UnsubscribeAll(this);
|
||||||
|
CancelAndDispose(ref _loopCts);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NearbyEntry> SnapshotEntries()
|
||||||
|
{
|
||||||
|
lock (_entriesLock)
|
||||||
|
{
|
||||||
|
return _lastEntries.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSnapshot(List<NearbyEntry> entries)
|
||||||
|
{
|
||||||
|
lock (_entriesLock)
|
||||||
|
{
|
||||||
|
_lastEntries = entries.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Loop(CancellationToken ct)
|
||||||
|
{
|
||||||
|
_configProvider.TryLoadFromStapled();
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool currentState = _config.Current.EnableAutoDetectDiscovery;
|
||||||
|
if (currentState != _lastAutoDetectState)
|
||||||
|
{
|
||||||
|
_lastAutoDetectState = currentState;
|
||||||
|
if (currentState)
|
||||||
|
{
|
||||||
|
// Force immediate publish on toggle ON
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure well-known is present
|
||||||
|
if (!_configProvider.HasConfig || _configProvider.IsExpired())
|
||||||
|
{
|
||||||
|
if (!_configProvider.TryLoadFromStapled())
|
||||||
|
{
|
||||||
|
await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ep = _configProvider.PublishEndpoint;
|
||||||
|
var saltBytes = _configProvider.Salt;
|
||||||
|
if (!string.IsNullOrEmpty(ep) && saltBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var saltHex = Convert.ToHexString(saltBytes);
|
||||||
|
string? displayName = null;
|
||||||
|
ushort meWorld = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
if (me != null)
|
||||||
|
{
|
||||||
|
displayName = me.Name.TextValue;
|
||||||
|
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
|
||||||
|
meWorld = (ushort)mePc.HomeWorld.RowId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(displayName))
|
||||||
|
{
|
||||||
|
var selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
|
||||||
|
_lastPublishedSignature = null; // ensure future loop doesn't skip
|
||||||
|
var okNow = await _api.PublishAsync(ep!, new[] { selfHash }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
|
||||||
|
_logger.LogInformation("Nearby immediate publish on toggle ON: {result}", okNow ? "success" : "failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Nearby immediate publish on toggle ON failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_notifiedEnabled)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect enabled : you are now visible.", default));
|
||||||
|
_notifiedEnabled = true;
|
||||||
|
_notifiedDisabled = false;
|
||||||
|
_disableSent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ep = _configProvider.PublishEndpoint;
|
||||||
|
if (!string.IsNullOrEmpty(ep) && !_disableSent)
|
||||||
|
{
|
||||||
|
var disableUrl = ep.Replace("/publish", "/disable");
|
||||||
|
try { await _api.DisableAsync(disableUrl, ct).ConfigureAwait(false); _disableSent = true; } catch { }
|
||||||
|
}
|
||||||
|
if (!_notifiedDisabled)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect disabled : you are not visible.", default));
|
||||||
|
_notifiedDisabled = true;
|
||||||
|
_notifiedEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!_config.Current.EnableAutoDetectDiscovery || !_dalamud.IsLoggedIn || !_isConnected)
|
||||||
|
{
|
||||||
|
if (!_config.Current.EnableAutoDetectDiscovery && !string.IsNullOrEmpty(_configProvider.PublishEndpoint))
|
||||||
|
{
|
||||||
|
var disableUrl = _configProvider.PublishEndpoint.Replace("/publish", "/disable");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_disableSent)
|
||||||
|
{
|
||||||
|
await _api.DisableAsync(disableUrl, ct).ConfigureAwait(false);
|
||||||
|
_disableSent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (!_notifiedDisabled)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect disabled : you are not visible.", default));
|
||||||
|
_notifiedDisabled = true;
|
||||||
|
_notifiedEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Task.Delay(1000, ct).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!_configProvider.HasConfig || _configProvider.IsExpired())
|
||||||
|
{
|
||||||
|
if (!_configProvider.TryLoadFromStapled())
|
||||||
|
{
|
||||||
|
await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!_loggedConfigReady && _configProvider.NearbyEnabled)
|
||||||
|
{
|
||||||
|
_loggedConfigReady = true;
|
||||||
|
_logger.LogInformation("Nearby: well-known loaded and enabled; refresh={refresh}s, expires={exp}", _configProvider.RefreshSec, _configProvider.SaltExpiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = await GetLocalNearbyAsync().ConfigureAwait(false);
|
||||||
|
// Log when local count changes (including 0) to indicate activity
|
||||||
|
if (entries.Count != _lastLocalCount)
|
||||||
|
{
|
||||||
|
_lastLocalCount = entries.Count;
|
||||||
|
_logger.LogTrace("Nearby: {count} players detected locally", _lastLocalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try query server if config and endpoints are present
|
||||||
|
if (_configProvider.NearbyEnabled && !_configProvider.IsExpired() &&
|
||||||
|
_configProvider.Salt is { Length: > 0 })
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var saltHex = Convert.ToHexString(_configProvider.Salt!);
|
||||||
|
// map hash->index for result matching
|
||||||
|
Dictionary<string, int> hashToIndex = new(StringComparer.Ordinal);
|
||||||
|
List<string> hashes = new(entries.Count);
|
||||||
|
foreach (var (entry, idx) in entries.Select((e, i) => (e, i)))
|
||||||
|
{
|
||||||
|
var h = (saltHex + entry.Name + entry.WorldId.ToString()).GetHash256();
|
||||||
|
hashToIndex[h] = idx;
|
||||||
|
hashes.Add(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapSig = string.Join(',', hashes.OrderBy(s => s, StringComparer.Ordinal)).GetHash256();
|
||||||
|
if (!string.Equals(snapSig, _lastSnapshotSig, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lastSnapshotSig = snapSig;
|
||||||
|
var sample = entries.Take(5).Select(e =>
|
||||||
|
{
|
||||||
|
var hh = (saltHex + e.Name + e.WorldId.ToString()).GetHash256();
|
||||||
|
var shortH = hh.Length > 8 ? hh[..8] : hh;
|
||||||
|
return $"{e.Name}({e.WorldId})->{shortH}";
|
||||||
|
});
|
||||||
|
var saltShort = saltHex.Length > 8 ? saltHex[..8] : saltHex;
|
||||||
|
_logger.LogTrace("Nearby snapshot: {count} entries; salt={saltShort}…; samples=[{samples}]",
|
||||||
|
entries.Count, saltShort, string.Join(", ", sample));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_configProvider.PublishEndpoint))
|
||||||
|
{
|
||||||
|
string? displayName = null;
|
||||||
|
string? selfHash = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
if (me != null)
|
||||||
|
{
|
||||||
|
displayName = me.Name.TextValue;
|
||||||
|
ushort meWorld = 0;
|
||||||
|
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
|
||||||
|
meWorld = (ushort)mePc.HomeWorld.RowId;
|
||||||
|
_logger.LogTrace("Nearby self ident: {name} ({world})", displayName, meWorld);
|
||||||
|
selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(selfHash))
|
||||||
|
{
|
||||||
|
var sig = selfHash!;
|
||||||
|
if (!string.Equals(sig, _lastPublishedSignature, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lastPublishedSignature = sig;
|
||||||
|
var shortSelf = selfHash!.Length > 8 ? selfHash[..8] : selfHash;
|
||||||
|
_logger.LogDebug("Nearby publish: self presence updated (hash={hash})", shortSelf);
|
||||||
|
var ok = await _api.PublishAsync(_configProvider.PublishEndpoint!, new[] { selfHash! }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
|
||||||
|
_logger.LogInformation("Nearby publish result: {result}", ok ? "success" : "failed");
|
||||||
|
if (ok) _lastHeartbeat = DateTime.UtcNow;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
if (!_notifiedEnabled)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect enabled : you are now visible.", default));
|
||||||
|
_notifiedEnabled = true;
|
||||||
|
_notifiedDisabled = false;
|
||||||
|
_disableSent = false; // allow future /disable when turning off again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No changes; perform heartbeat publish if interval elapsed
|
||||||
|
if (DateTime.UtcNow - _lastHeartbeat >= HeartbeatInterval)
|
||||||
|
{
|
||||||
|
var okHb = await _api.PublishAsync(_configProvider.PublishEndpoint!, new[] { selfHash! }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Nearby heartbeat publish: {result}", okHb ? "success" : "failed");
|
||||||
|
if (okHb) _lastHeartbeat = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Nearby publish skipped (no changes)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else: no self character available; skip publish silently
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for matches if endpoint is available
|
||||||
|
if (!string.IsNullOrEmpty(_configProvider.QueryEndpoint))
|
||||||
|
{
|
||||||
|
// chunked queries
|
||||||
|
int batch = Math.Max(1, _configProvider.MaxQueryBatch);
|
||||||
|
List<ServerMatch> allMatches = new();
|
||||||
|
for (int i = 0; i < hashes.Count; i += batch)
|
||||||
|
{
|
||||||
|
var slice = hashes.Skip(i).Take(batch).ToArray();
|
||||||
|
var res = await _api.QueryAsync(_configProvider.QueryEndpoint!, slice, ct).ConfigureAwait(false);
|
||||||
|
if (res != null && res.Count > 0) allMatches.AddRange(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allMatches.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var m in allMatches)
|
||||||
|
{
|
||||||
|
if (hashToIndex.TryGetValue(m.Hash, out var idx))
|
||||||
|
{
|
||||||
|
var e = entries[idx];
|
||||||
|
entries[idx] = new NearbyEntry(e.Name, e.WorldId, e.Distance, true, m.Token, m.DisplayName, m.Uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allMatches.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Nearby: server returned {count} matches", allMatches.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Nearby: server returned {count} matches", allMatches.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log change in number of Umbra matches
|
||||||
|
int matchCount = entries.Count(e => e.IsMatch);
|
||||||
|
if (matchCount != _lastMatchCount)
|
||||||
|
{
|
||||||
|
_lastMatchCount = matchCount;
|
||||||
|
if (matchCount > 0)
|
||||||
|
{
|
||||||
|
var matchSamples = entries.Where(e => e.IsMatch).Take(5)
|
||||||
|
.Select(e => string.IsNullOrEmpty(e.DisplayName) ? e.Name : e.DisplayName!);
|
||||||
|
_logger.LogInformation("Nearby: {count} Umbra users nearby [{samples}]",
|
||||||
|
matchCount, string.Join(", ", matchSamples));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Nearby: {count} Umbra users nearby", matchCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Nearby query failed; falling back to local list");
|
||||||
|
if (ex.Message.Contains("DISCOVERY_SALT_EXPIRED", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Nearby: salt expired, refetching well-known");
|
||||||
|
try { await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_loggedLocalOnly)
|
||||||
|
{
|
||||||
|
_loggedLocalOnly = true;
|
||||||
|
_logger.LogDebug("Nearby: well-known not available or disabled; running in local-only mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateSnapshot(entries);
|
||||||
|
_mediator.Publish(new DiscoveryListUpdated(entries));
|
||||||
|
|
||||||
|
var delayMs = Math.Max(1000, _configProvider.MinQueryIntervalMs);
|
||||||
|
if (entries.Count == 0) delayMs = Math.Max(delayMs, 5000);
|
||||||
|
await Task.Delay(delayMs, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "NearbyDiscoveryService loop error");
|
||||||
|
await Task.Delay(2000, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<NearbyEntry>> GetLocalNearbyAsync()
|
||||||
|
{
|
||||||
|
var list = new List<NearbyEntry>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var local = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
var localPos = local?.Position ?? Vector3.Zero;
|
||||||
|
int maxDist = Math.Clamp(_config.Current.AutoDetectMaxDistanceMeters, 5, 100);
|
||||||
|
|
||||||
|
int limit = Math.Min(200, _objectTable.Length);
|
||||||
|
for (int i = 0; i < limit; i++)
|
||||||
|
{
|
||||||
|
var obj = await _dalamud.RunOnFrameworkThread(() => _objectTable[i]).ConfigureAwait(false);
|
||||||
|
if (obj == null || obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
|
||||||
|
if (local != null && obj.Address == local.Address) continue;
|
||||||
|
|
||||||
|
float dist = local == null ? float.NaN : Vector3.Distance(localPos, obj.Position);
|
||||||
|
if (!float.IsNaN(dist) && dist > maxDist) continue;
|
||||||
|
|
||||||
|
string name = obj.Name.TextValue;
|
||||||
|
ushort worldId = 0;
|
||||||
|
if (obj is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter pc)
|
||||||
|
worldId = (ushort)pc.HomeWorld.RowId;
|
||||||
|
|
||||||
|
list.Add(new NearbyEntry(name, worldId, dist, false, null, null, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
Normal file
116
MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.Notifications;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public sealed class NearbyPendingService : IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly ILogger<NearbyPendingService> _logger;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly ApiController _api;
|
||||||
|
private readonly AutoDetectRequestService _requestService;
|
||||||
|
private readonly NotificationTracker _notificationTracker;
|
||||||
|
private readonly ConcurrentDictionary<string, string> _pending = new(StringComparer.Ordinal);
|
||||||
|
private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
|
||||||
|
private static readonly Regex ReqRegex = new(@"^Nearby Request: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
|
||||||
|
private static readonly Regex AcceptRegex = new(@"^Nearby Accept: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
|
||||||
|
|
||||||
|
public NearbyPendingService(ILogger<NearbyPendingService> logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService, NotificationTracker notificationTracker)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
_api = api;
|
||||||
|
_requestService = requestService;
|
||||||
|
_notificationTracker = notificationTracker;
|
||||||
|
_mediator.Subscribe<NotificationMessage>(this, OnNotification);
|
||||||
|
_mediator.Subscribe<ManualPairInviteMessage>(this, OnManualPairInvite);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator => _mediator;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string> Pending => _pending;
|
||||||
|
|
||||||
|
private void OnNotification(NotificationMessage msg)
|
||||||
|
{
|
||||||
|
// Watch info messages for Nearby request pattern
|
||||||
|
if (msg.Type != MareSynchronos.MareConfiguration.Models.NotificationType.Info) return;
|
||||||
|
var ma = AcceptRegex.Match(msg.Message);
|
||||||
|
if (ma.Success)
|
||||||
|
{
|
||||||
|
var uidA = ma.Groups["uid"].Value;
|
||||||
|
if (!string.IsNullOrEmpty(uidA))
|
||||||
|
{
|
||||||
|
_ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
|
||||||
|
_pending.TryRemove(uidA, out _);
|
||||||
|
_requestService.RemovePendingRequestByUid(uidA);
|
||||||
|
_notificationTracker.Remove(NotificationCategory.AutoDetect, uidA);
|
||||||
|
_logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = ReqRegex.Match(msg.Message);
|
||||||
|
if (!m.Success) return;
|
||||||
|
var uid = m.Groups["uid"].Value;
|
||||||
|
if (string.IsNullOrEmpty(uid)) return;
|
||||||
|
// Try to extract name as everything before space and '['
|
||||||
|
var name = msg.Message;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var idx = msg.Message.IndexOf(':');
|
||||||
|
if (idx >= 0) name = msg.Message[(idx + 1)..].Trim();
|
||||||
|
var br = name.LastIndexOf('[');
|
||||||
|
if (br > 0) name = name[..br].Trim();
|
||||||
|
}
|
||||||
|
catch { name = uid; }
|
||||||
|
_pending[uid] = name;
|
||||||
|
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name);
|
||||||
|
_notificationTracker.Upsert(NotificationEntry.AutoDetect(uid, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnManualPairInvite(ManualPairInviteMessage msg)
|
||||||
|
{
|
||||||
|
if (!string.Equals(msg.TargetUid, _api.UID, StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var display = !string.IsNullOrWhiteSpace(msg.DisplayName)
|
||||||
|
? msg.DisplayName!
|
||||||
|
: (!string.IsNullOrWhiteSpace(msg.SourceAlias) ? msg.SourceAlias : msg.SourceUid);
|
||||||
|
|
||||||
|
_pending[msg.SourceUid] = display;
|
||||||
|
_logger.LogInformation("NearbyPending: received manual invite from {uid} ({name})", msg.SourceUid, display);
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request", $"{display} vous a envoyé une invitation de pair.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
|
_notificationTracker.Upsert(NotificationEntry.AutoDetect(msg.SourceUid, display));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string uid)
|
||||||
|
{
|
||||||
|
_pending.TryRemove(uid, out _);
|
||||||
|
_requestService.RemovePendingRequestByUid(uid);
|
||||||
|
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AcceptAsync(string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uid))).ConfigureAwait(false);
|
||||||
|
_pending.TryRemove(uid, out _);
|
||||||
|
_requestService.RemovePendingRequestByUid(uid);
|
||||||
|
_ = _requestService.SendAcceptNotifyAsync(uid);
|
||||||
|
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "NearbyPending: accept failed for {uid}", uid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
158
MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs
Normal file
158
MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public sealed class SyncshellDiscoveryService : IHostedService, IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly ILogger<SyncshellDiscoveryService> _logger;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly SemaphoreSlim _refreshSemaphore = new(1, 1);
|
||||||
|
private readonly object _entriesLock = new();
|
||||||
|
private List<SyncshellDiscoveryEntryDto> _entries = [];
|
||||||
|
private string? _lastError;
|
||||||
|
private bool _isRefreshing;
|
||||||
|
|
||||||
|
public SyncshellDiscoveryService(ILogger<SyncshellDiscoveryService> logger, MareMediator mediator, ApiController apiController)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
_apiController = apiController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator => _mediator;
|
||||||
|
|
||||||
|
public IReadOnlyList<SyncshellDiscoveryEntryDto> Entries
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_entriesLock)
|
||||||
|
{
|
||||||
|
return _entries.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRefreshing => _isRefreshing;
|
||||||
|
public string? LastError => _lastError;
|
||||||
|
|
||||||
|
public async Task<bool> JoinAsync(string gid, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _apiController.SyncshellDiscoveryJoin(new GroupDto(new GroupData(gid))).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Join syncshell discovery failed for {gid}", gid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncshellDiscoveryStateDto?> GetStateAsync(string gid, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _apiController.SyncshellDiscoveryGetState(new GroupDto(new GroupData(gid))).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to fetch syncshell discovery state for {gid}", gid);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetVisibilityAsync(string gid, bool visible, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await SetVisibilityAsync(gid, visible, null, null, null, null, null, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetVisibilityAsync(string gid, bool visible, int? displayDurationHours,
|
||||||
|
int[]? activeWeekdays, TimeSpan? timeStartLocal, TimeSpan? timeEndLocal, string? timeZone,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new SyncshellDiscoveryVisibilityRequestDto
|
||||||
|
{
|
||||||
|
GID = gid,
|
||||||
|
AutoDetectVisible = visible,
|
||||||
|
DisplayDurationHours = displayDurationHours,
|
||||||
|
ActiveWeekdays = activeWeekdays,
|
||||||
|
TimeStartLocal = timeStartLocal.HasValue ? new DateTime(timeStartLocal.Value.Ticks).ToString("HH:mm") : null,
|
||||||
|
TimeEndLocal = timeEndLocal.HasValue ? new DateTime(timeEndLocal.Value.Ticks).ToString("HH:mm") : null,
|
||||||
|
TimeZone = timeZone,
|
||||||
|
};
|
||||||
|
var success = await _apiController.SyncshellDiscoverySetVisibility(request).ConfigureAwait(false);
|
||||||
|
if (!success) return false;
|
||||||
|
|
||||||
|
var state = await GetStateAsync(gid, ct).ConfigureAwait(false);
|
||||||
|
if (state != null)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new SyncshellAutoDetectStateChanged(state.GID, state.AutoDetectVisible, state.PasswordTemporarilyDisabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to set syncshell visibility for {gid}", gid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!await _refreshSemaphore.WaitAsync(0, ct).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isRefreshing = true;
|
||||||
|
var discovered = await _apiController.SyncshellDiscoveryList().ConfigureAwait(false);
|
||||||
|
lock (_entriesLock)
|
||||||
|
{
|
||||||
|
_entries = discovered ?? [];
|
||||||
|
}
|
||||||
|
_lastError = null;
|
||||||
|
_mediator.Publish(new SyncshellDiscoveryUpdated(Entries.ToList()));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to refresh syncshell discovery list");
|
||||||
|
_lastError = ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isRefreshing = false;
|
||||||
|
_refreshSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.Subscribe<ConnectedMessage>(this, msg =>
|
||||||
|
{
|
||||||
|
_ = RefreshAsync(CancellationToken.None);
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.UnsubscribeAll(this);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ using MareSynchronos.Services.CharaData.Models;
|
|||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using MareSynchronos.WebAPI.Files;
|
using MareSynchronos.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
@@ -295,6 +296,32 @@ public sealed class CharaDataFileHandler : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal async Task<byte[]?> CreateCharaFileBytesAsync(string description, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SaveCharaFileAsync(description, tempFilePath).ConfigureAwait(false);
|
||||||
|
if (!File.Exists(tempFilePath)) return null;
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
return await File.ReadAllBytesAsync(tempFilePath, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(tempFilePath))
|
||||||
|
{
|
||||||
|
File.Delete(tempFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
||||||
{
|
{
|
||||||
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ using MareSynchronos.Utils;
|
|||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
@@ -457,6 +459,14 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
|
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> LoadMcdfFromBytes(byte[] data, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
|
||||||
|
await File.WriteAllBytesAsync(tempFilePath, data, token).ConfigureAwait(false);
|
||||||
|
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(tempFilePath);
|
||||||
|
return tempFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
public void McdfApplyToTarget(string charaName)
|
public void McdfApplyToTarget(string charaName)
|
||||||
{
|
{
|
||||||
if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;
|
if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
|||||||
private Task? _filterEntriesRunningTask;
|
private Task? _filterEntriesRunningTask;
|
||||||
private (Guid VfxId, PoseEntryExtended Pose)? _hoveredVfx = null;
|
private (Guid VfxId, PoseEntryExtended Pose)? _hoveredVfx = null;
|
||||||
private DateTime _lastExecutionTime = DateTime.UtcNow;
|
private DateTime _lastExecutionTime = DateTime.UtcNow;
|
||||||
private SemaphoreSlim _sharedDataUpdateSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _sharedDataUpdateSemaphore = new(1, 1);
|
||||||
public CharaDataNearbyManager(ILogger<CharaDataNearbyManager> logger, MareMediator mediator,
|
public CharaDataNearbyManager(ILogger<CharaDataNearbyManager> logger, MareMediator mediator,
|
||||||
DalamudUtilService dalamudUtilService, VfxSpawnManager vfxSpawnManager,
|
DalamudUtilService dalamudUtilService, VfxSpawnManager vfxSpawnManager,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
@@ -201,7 +201,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
|||||||
foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter)
|
foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter)
|
||||||
|| ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
|| ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
||||||
|| d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
|| d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
||||||
|| (_serverConfigurationManager.GetNoteForUid(UserNoteFilter) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
|
|| (_serverConfigurationManager.GetNoteForUid(d.Key.UID) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
|
||||||
.ToDictionary(k => k.Key, k => k.Value))
|
.ToDictionary(k => k.Key, k => k.Value))
|
||||||
{
|
{
|
||||||
// filter all poses based on territory, that always must be correct
|
// filter all poses based on territory, that always must be correct
|
||||||
@@ -266,26 +266,47 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses)
|
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses)
|
||||||
{
|
{
|
||||||
foreach (var data in _nearbyData.Keys)
|
int maxWisps = _charaDataConfigService.Current.NearbyMaxWisps;
|
||||||
|
if (maxWisps <= 0)
|
||||||
{
|
{
|
||||||
if (_poseVfx.TryGetValue(data, out var _)) continue;
|
ClearAllVfx();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int hardLimit = 200;
|
||||||
|
if (maxWisps > hardLimit) maxWisps = hardLimit;
|
||||||
|
|
||||||
|
var orderedAllowedPoses = _nearbyData
|
||||||
|
.OrderBy(kvp => kvp.Value.Distance)
|
||||||
|
.Take(maxWisps)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var allowedPoseSet = orderedAllowedPoses.ToHashSet();
|
||||||
|
|
||||||
|
foreach (var data in orderedAllowedPoses)
|
||||||
|
{
|
||||||
|
if (_poseVfx.TryGetValue(data, out _)) continue;
|
||||||
|
|
||||||
|
Guid? vfxGuid = data.MetaInfo.IsOwnData
|
||||||
|
? _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f)
|
||||||
|
: _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
|
||||||
|
|
||||||
Guid? vfxGuid;
|
|
||||||
if (data.MetaInfo.IsOwnData)
|
|
||||||
{
|
|
||||||
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
|
|
||||||
}
|
|
||||||
if (vfxGuid != null)
|
if (vfxGuid != null)
|
||||||
{
|
{
|
||||||
_poseVfx[data] = vfxGuid.Value;
|
_poseVfx[data] = vfxGuid.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var data in previousPoses.Except(_nearbyData.Keys))
|
foreach (var data in previousPoses.Except(allowedPoseSet))
|
||||||
|
{
|
||||||
|
if (_poseVfx.Remove(data, out var guid))
|
||||||
|
{
|
||||||
|
_vfxSpawnManager.DespawnObject(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var data in _poseVfx.Keys.Except(allowedPoseSet).ToList())
|
||||||
{
|
{
|
||||||
if (_poseVfx.Remove(data, out var guid))
|
if (_poseVfx.Remove(data, out var guid))
|
||||||
{
|
{
|
||||||
|
|||||||
309
MareSynchronos/Services/CharaData/McdfShareManager.cs
Normal file
309
MareSynchronos/Services/CharaData/McdfShareManager.cs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MareSynchronos.API.Dto.McdfShare;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData;
|
||||||
|
|
||||||
|
public sealed class McdfShareManager
|
||||||
|
{
|
||||||
|
private readonly ILogger<McdfShareManager> _logger;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly CharaDataFileHandler _fileHandler;
|
||||||
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly SemaphoreSlim _operationSemaphore = new(1, 1);
|
||||||
|
private readonly List<McdfShareEntryDto> _ownShares = new();
|
||||||
|
private readonly List<McdfShareEntryDto> _sharedWithMe = new();
|
||||||
|
private Task? _currentTask;
|
||||||
|
|
||||||
|
public McdfShareManager(ILogger<McdfShareManager> logger, ApiController apiController,
|
||||||
|
CharaDataFileHandler fileHandler, CharaDataManager charaDataManager,
|
||||||
|
ServerConfigurationManager serverConfigurationManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_apiController = apiController;
|
||||||
|
_fileHandler = fileHandler;
|
||||||
|
_charaDataManager = charaDataManager;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<McdfShareEntryDto> OwnShares => _ownShares;
|
||||||
|
public IReadOnlyList<McdfShareEntryDto> SharedShares => _sharedWithMe;
|
||||||
|
public bool IsBusy => _currentTask is { IsCompleted: false };
|
||||||
|
public string? LastError { get; private set; }
|
||||||
|
public string? LastSuccess { get; private set; }
|
||||||
|
|
||||||
|
public Task RefreshAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(() => InternalRefreshAsync(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateShareAsync(string description, IReadOnlyList<string> allowedIndividuals, IReadOnlyList<string> allowedSyncshells, DateTime? expiresAtUtc, CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var mcdfBytes = await _fileHandler.CreateCharaFileBytesAsync(description, token).ConfigureAwait(false);
|
||||||
|
if (mcdfBytes == null || mcdfBytes.Length == 0)
|
||||||
|
{
|
||||||
|
LastError = "Impossible de préparer les données MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
|
||||||
|
if (hasMultiple)
|
||||||
|
{
|
||||||
|
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage. Corrigez cela dans les paramètres.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(secretKey))
|
||||||
|
{
|
||||||
|
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shareId = Guid.NewGuid();
|
||||||
|
byte[] salt = RandomNumberGenerator.GetBytes(16);
|
||||||
|
byte[] nonce = RandomNumberGenerator.GetBytes(12);
|
||||||
|
byte[] key = DeriveKey(secretKey, shareId, salt);
|
||||||
|
|
||||||
|
byte[] cipher = new byte[mcdfBytes.Length];
|
||||||
|
byte[] tag = new byte[16];
|
||||||
|
|
||||||
|
using (var aes = new AesGcm(key, 16))
|
||||||
|
{
|
||||||
|
aes.Encrypt(nonce, mcdfBytes, cipher, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadDto = new McdfShareUploadRequestDto
|
||||||
|
{
|
||||||
|
ShareId = shareId,
|
||||||
|
Description = description,
|
||||||
|
CipherData = cipher,
|
||||||
|
Nonce = nonce,
|
||||||
|
Salt = salt,
|
||||||
|
Tag = tag,
|
||||||
|
ExpiresAtUtc = expiresAtUtc,
|
||||||
|
AllowedIndividuals = allowedIndividuals.ToList(),
|
||||||
|
AllowedSyncshells = allowedSyncshells.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
await _apiController.McdfShareUpload(uploadDto).ConfigureAwait(false);
|
||||||
|
await InternalRefreshAsync(token).ConfigureAwait(false);
|
||||||
|
LastSuccess = "Partage MCDF créé.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteShareAsync(Guid shareId)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
var result = await _apiController.McdfShareDelete(shareId).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
LastError = "Le serveur a refusé de supprimer le partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ownShares.RemoveAll(s => s.Id == shareId);
|
||||||
|
_sharedWithMe.RemoveAll(s => s.Id == shareId);
|
||||||
|
await InternalRefreshAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
LastSuccess = "Partage MCDF supprimé.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateShareAsync(McdfShareUpdateRequestDto updateRequest)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
var updated = await _apiController.McdfShareUpdate(updateRequest).ConfigureAwait(false);
|
||||||
|
if (updated == null)
|
||||||
|
{
|
||||||
|
LastError = "Le serveur a refusé de mettre à jour le partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = _ownShares.FindIndex(s => s.Id == updated.Id);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
_ownShares[idx] = updated;
|
||||||
|
}
|
||||||
|
LastSuccess = "Partage MCDF mis à jour.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ApplyShareAsync(Guid shareId, CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
|
||||||
|
if (plainBytes == null)
|
||||||
|
{
|
||||||
|
LastError ??= "Échec du téléchargement du partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempPath = await _charaDataManager.LoadMcdfFromBytes(plainBytes, token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _charaDataManager.McdfApplyToGposeTarget().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(tempPath))
|
||||||
|
{
|
||||||
|
File.Delete(tempPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LastSuccess = "Partage MCDF appliqué sur la cible GPose.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExportShareAsync(Guid shareId, string filePath, CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
|
||||||
|
if (plainBytes == null)
|
||||||
|
{
|
||||||
|
LastError ??= "Échec du téléchargement du partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(filePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllBytesAsync(filePath, plainBytes, token).ConfigureAwait(false);
|
||||||
|
LastSuccess = "Partage MCDF exporté.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DownloadShareToFileAsync(McdfShareEntryDto entry, string filePath, CancellationToken token)
|
||||||
|
{
|
||||||
|
return ExportShareAsync(entry.Id, filePath, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]?> DownloadAndDecryptShareAsync(Guid shareId, CancellationToken token)
|
||||||
|
{
|
||||||
|
var payload = await _apiController.McdfShareDownload(shareId).ConfigureAwait(false);
|
||||||
|
if (payload == null)
|
||||||
|
{
|
||||||
|
LastError = "Partage indisponible.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
|
||||||
|
if (hasMultiple)
|
||||||
|
{
|
||||||
|
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(secretKey))
|
||||||
|
{
|
||||||
|
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] key = DeriveKey(secretKey, payload.ShareId, payload.Salt);
|
||||||
|
byte[] plaintext = new byte[payload.CipherData.Length];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var aes = new AesGcm(key, 16);
|
||||||
|
aes.Decrypt(payload.Nonce, payload.CipherData, payload.Tag, plaintext);
|
||||||
|
}
|
||||||
|
catch (CryptographicException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to decrypt MCDF share {ShareId}", shareId);
|
||||||
|
LastError = "Impossible de déchiffrer le partage MCDF.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InternalRefreshAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var own = await _apiController.McdfShareGetOwn().ConfigureAwait(false);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var shared = await _apiController.McdfShareGetShared().ConfigureAwait(false);
|
||||||
|
_ownShares.Clear();
|
||||||
|
_ownShares.AddRange(own);
|
||||||
|
_sharedWithMe.Clear();
|
||||||
|
_sharedWithMe.AddRange(shared);
|
||||||
|
LastSuccess = "Partages MCDF actualisés.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task RunOperation(Func<Task> operation)
|
||||||
|
{
|
||||||
|
async Task Wrapper()
|
||||||
|
{
|
||||||
|
await _operationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LastError = null;
|
||||||
|
LastSuccess = null;
|
||||||
|
await operation().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during MCDF share operation");
|
||||||
|
LastError = ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_operationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task = Wrapper();
|
||||||
|
_currentTask = task;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] DeriveKey(string secretKey, Guid shareId, byte[] salt)
|
||||||
|
{
|
||||||
|
byte[] secretBytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
secretBytes = Convert.FromHexString(secretKey);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
// fallback to UTF8 if not hex
|
||||||
|
secretBytes = System.Text.Encoding.UTF8.GetBytes(secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] shareBytes = shareId.ToByteArray();
|
||||||
|
byte[] material = new byte[secretBytes.Length + shareBytes.Length + salt.Length];
|
||||||
|
Buffer.BlockCopy(secretBytes, 0, material, 0, secretBytes.Length);
|
||||||
|
Buffer.BlockCopy(shareBytes, 0, material, secretBytes.Length, shareBytes.Length);
|
||||||
|
Buffer.BlockCopy(salt, 0, material, secretBytes.Length + shareBytes.Length, salt.Length);
|
||||||
|
return SHA256.HashData(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
using Lumina.Data.Files;
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Lumina.Data.Files;
|
||||||
using MareSynchronos.API.Data;
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.API.Data.Enum;
|
using MareSynchronos.API.Data.Enum;
|
||||||
using MareSynchronos.FileCache;
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
@@ -14,40 +18,52 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
||||||
private CancellationTokenSource? _analysisCts;
|
private CancellationTokenSource? _analysisCts;
|
||||||
private CancellationTokenSource _baseAnalysisCts = new();
|
private CancellationTokenSource? _baseAnalysisCts = new();
|
||||||
private string _lastDataHash = string.Empty;
|
private string _lastDataHash = string.Empty;
|
||||||
|
private CharacterAnalysisSummary _previousSummary = CharacterAnalysisSummary.Empty;
|
||||||
|
private DateTime _lastAutoAnalysis = DateTime.MinValue;
|
||||||
|
private string _lastAutoAnalysisHash = string.Empty;
|
||||||
|
private const int AutoAnalysisFileDeltaThreshold = 25;
|
||||||
|
private const long AutoAnalysisSizeDeltaThreshold = 50L * 1024 * 1024;
|
||||||
|
private static readonly TimeSpan AutoAnalysisCooldown = TimeSpan.FromMinutes(2);
|
||||||
|
private const long NotificationSizeThreshold = 300L * 1024 * 1024;
|
||||||
|
private const long NotificationTriangleThreshold = 150_000;
|
||||||
|
private bool _sizeWarningShown;
|
||||||
|
private bool _triangleWarningShown;
|
||||||
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
|
|
||||||
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer, PlayerPerformanceConfigService playerPerformanceConfigService)
|
||||||
: base(logger, mediator)
|
: base(logger, mediator)
|
||||||
{
|
{
|
||||||
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
|
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
|
var tokenSource = EnsureFreshCts(ref _baseAnalysisCts);
|
||||||
var token = _baseAnalysisCts.Token;
|
var token = tokenSource.Token;
|
||||||
_ = BaseAnalysis(msg.CharacterData, token);
|
_ = BaseAnalysis(msg.CharacterData, token);
|
||||||
});
|
});
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_xivDataAnalyzer = modelAnalyzer;
|
_xivDataAnalyzer = modelAnalyzer;
|
||||||
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CurrentFile { get; internal set; }
|
public int CurrentFile { get; internal set; }
|
||||||
public bool IsAnalysisRunning => _analysisCts != null;
|
public bool IsAnalysisRunning => _analysisCts != null;
|
||||||
public int TotalFiles { get; internal set; }
|
public int TotalFiles { get; internal set; }
|
||||||
|
public CharacterAnalysisSummary CurrentSummary { get; private set; } = CharacterAnalysisSummary.Empty;
|
||||||
|
public DateTime? LastCompletedAnalysis { get; private set; }
|
||||||
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
|
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
|
||||||
|
|
||||||
public void CancelAnalyze()
|
public void CancelAnalyze()
|
||||||
{
|
{
|
||||||
_analysisCts?.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_analysisCts = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
|
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("=== Calculating Character Analysis ===");
|
Logger.LogDebug("=== Calculating Character Analysis ===");
|
||||||
|
|
||||||
_analysisCts = _analysisCts?.CancelRecreate() ?? new();
|
var analysisCts = EnsureFreshCts(ref _analysisCts);
|
||||||
|
var cancelToken = analysisCts.Token;
|
||||||
var cancelToken = _analysisCts.Token;
|
|
||||||
|
|
||||||
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
|
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
|
||||||
if (allFiles.Exists(c => !c.IsComputed || recalculate))
|
if (allFiles.Exists(c => !c.IsComputed || recalculate))
|
||||||
@@ -80,10 +96,16 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefreshSummary(false, _lastDataHash);
|
||||||
|
|
||||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||||
|
|
||||||
_analysisCts.CancelDispose();
|
if (!cancelToken.IsCancellationRequested)
|
||||||
_analysisCts = null;
|
{
|
||||||
|
LastCompletedAnalysis = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelAndDispose(ref _analysisCts);
|
||||||
|
|
||||||
if (print) PrintAnalysis();
|
if (print) PrintAnalysis();
|
||||||
}
|
}
|
||||||
@@ -94,8 +116,8 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!disposing) return;
|
if (!disposing) return;
|
||||||
|
|
||||||
_analysisCts?.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_baseAnalysisCts.CancelDispose();
|
CancelAndDispose(ref _baseAnalysisCts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||||
@@ -142,9 +164,11 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
LastAnalysis[obj.Key] = data;
|
LastAnalysis[obj.Key] = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lastDataHash = charaData.DataHash.Value;
|
||||||
|
RefreshSummary(true, _lastDataHash);
|
||||||
|
|
||||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||||
|
|
||||||
_lastDataHash = charaData.DataHash.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrintAnalysis()
|
private void PrintAnalysis()
|
||||||
@@ -193,6 +217,169 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshSummary(bool evaluateAutoAnalysis, string dataHash)
|
||||||
|
{
|
||||||
|
var summary = CalculateSummary();
|
||||||
|
CurrentSummary = summary;
|
||||||
|
|
||||||
|
if (evaluateAutoAnalysis)
|
||||||
|
{
|
||||||
|
EvaluateAutoAnalysis(summary, dataHash);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_previousSummary = summary;
|
||||||
|
|
||||||
|
if (!summary.HasUncomputedEntries && string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lastAutoAnalysisHash = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EvaluateThresholdNotifications(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharacterAnalysisSummary CalculateSummary()
|
||||||
|
{
|
||||||
|
if (LastAnalysis.Count == 0)
|
||||||
|
{
|
||||||
|
return CharacterAnalysisSummary.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
long original = 0;
|
||||||
|
long compressed = 0;
|
||||||
|
long triangles = 0;
|
||||||
|
int files = 0;
|
||||||
|
bool hasUncomputed = false;
|
||||||
|
|
||||||
|
foreach (var obj in LastAnalysis.Values)
|
||||||
|
{
|
||||||
|
foreach (var entry in obj.Values)
|
||||||
|
{
|
||||||
|
files++;
|
||||||
|
original += entry.OriginalSize;
|
||||||
|
compressed += entry.CompressedSize;
|
||||||
|
triangles += entry.Triangles;
|
||||||
|
hasUncomputed |= !entry.IsComputed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CharacterAnalysisSummary(files, original, compressed, triangles, hasUncomputed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EvaluateAutoAnalysis(CharacterAnalysisSummary newSummary, string dataHash)
|
||||||
|
{
|
||||||
|
var previous = _previousSummary;
|
||||||
|
_previousSummary = newSummary;
|
||||||
|
|
||||||
|
if (newSummary.TotalFiles == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAnalysisRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - _lastAutoAnalysis < AutoAnalysisCooldown)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool firstSummary = previous.TotalFiles == 0;
|
||||||
|
bool filesIncreased = newSummary.TotalFiles - previous.TotalFiles >= AutoAnalysisFileDeltaThreshold;
|
||||||
|
bool sizeIncreased = newSummary.TotalCompressedSize - previous.TotalCompressedSize >= AutoAnalysisSizeDeltaThreshold;
|
||||||
|
bool needsCompute = newSummary.HasUncomputedEntries;
|
||||||
|
|
||||||
|
if (!firstSummary && !filesIncreased && !sizeIncreased && !needsCompute)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastAutoAnalysis = now;
|
||||||
|
_lastAutoAnalysisHash = dataHash;
|
||||||
|
_ = ComputeAnalysis(print: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EvaluateThresholdNotifications(CharacterAnalysisSummary summary)
|
||||||
|
{
|
||||||
|
if (summary.IsEmpty || summary.HasUncomputedEntries)
|
||||||
|
{
|
||||||
|
ResetThresholdFlagsIfNeeded(summary);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_playerPerformanceConfigService.Current.ShowSelfAnalysisWarnings)
|
||||||
|
{
|
||||||
|
ResetThresholdFlagsIfNeeded(summary);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sizeExceeded = summary.TotalCompressedSize >= NotificationSizeThreshold;
|
||||||
|
bool trianglesExceeded = summary.TotalTriangles >= NotificationTriangleThreshold;
|
||||||
|
List<string> exceededReasons = new();
|
||||||
|
|
||||||
|
if (sizeExceeded && !_sizeWarningShown)
|
||||||
|
{
|
||||||
|
exceededReasons.Add($"un poids partagé de {UiSharedService.ByteToString(summary.TotalCompressedSize)} (≥ 300 MiB)");
|
||||||
|
_sizeWarningShown = true;
|
||||||
|
}
|
||||||
|
else if (!sizeExceeded && _sizeWarningShown)
|
||||||
|
{
|
||||||
|
_sizeWarningShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trianglesExceeded && !_triangleWarningShown)
|
||||||
|
{
|
||||||
|
exceededReasons.Add($"un total de {UiSharedService.TrisToString(summary.TotalTriangles)} triangles (≥ 150k)");
|
||||||
|
_triangleWarningShown = true;
|
||||||
|
}
|
||||||
|
else if (!trianglesExceeded && _triangleWarningShown)
|
||||||
|
{
|
||||||
|
_triangleWarningShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exceededReasons.Count == 0) return;
|
||||||
|
|
||||||
|
string combined = string.Join(" et ", exceededReasons);
|
||||||
|
string message = $"Attention : votre self-analysis indique {combined}. Des joueurs risquent de ne pas vous voir et UmbraSync peut activer un auto-pause. Pensez à réduire textures ou modèles lourds.";
|
||||||
|
Mediator.Publish(new DualNotificationMessage("Self Analysis", message, NotificationType.Warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetThresholdFlagsIfNeeded(CharacterAnalysisSummary summary)
|
||||||
|
{
|
||||||
|
if (summary.IsEmpty)
|
||||||
|
{
|
||||||
|
_sizeWarningShown = false;
|
||||||
|
_triangleWarningShown = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.TotalCompressedSize < NotificationSizeThreshold)
|
||||||
|
{
|
||||||
|
_sizeWarningShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.TotalTriangles < NotificationTriangleThreshold)
|
||||||
|
{
|
||||||
|
_triangleWarningShown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Auto)]
|
||||||
|
public readonly record struct CharacterAnalysisSummary(int TotalFiles, long TotalOriginalSize, long TotalCompressedSize, long TotalTriangles, bool HasUncomputedEntries)
|
||||||
|
{
|
||||||
|
public static CharacterAnalysisSummary Empty => new();
|
||||||
|
public bool IsEmpty => TotalFiles == 0 && TotalOriginalSize == 0 && TotalCompressedSize == 0 && TotalTriangles == 0;
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
|
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
|
||||||
{
|
{
|
||||||
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
||||||
@@ -239,4 +426,26 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
@@ -5,6 +8,7 @@ using Dalamud.Plugin.Services;
|
|||||||
using MareSynchronos.API.Data;
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.Interop;
|
using MareSynchronos.Interop;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
@@ -18,6 +22,7 @@ namespace MareSynchronos.Services;
|
|||||||
public class ChatService : DisposableMediatorSubscriberBase
|
public class ChatService : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
public const int DefaultColor = 710;
|
public const int DefaultColor = 710;
|
||||||
|
private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
|
||||||
public const int CommandMaxNumber = 50;
|
public const int CommandMaxNumber = 50;
|
||||||
|
|
||||||
private readonly ILogger<ChatService> _logger;
|
private readonly ILogger<ChatService> _logger;
|
||||||
@@ -30,6 +35,14 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private readonly Lazy<GameChatHooks> _gameChatHooks;
|
private readonly Lazy<GameChatHooks> _gameChatHooks;
|
||||||
|
|
||||||
|
private readonly object _typingLock = new();
|
||||||
|
private CancellationTokenSource? _typingCts;
|
||||||
|
private bool _isTypingAnnounced;
|
||||||
|
private DateTime _lastTypingSent = DateTime.MinValue;
|
||||||
|
private TypingScope _lastScope = TypingScope.Unknown;
|
||||||
|
private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2);
|
||||||
|
private static readonly TimeSpan TypingResendInterval = TimeSpan.FromMilliseconds(750);
|
||||||
|
|
||||||
public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController,
|
public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController,
|
||||||
PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui,
|
PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui,
|
||||||
MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator)
|
MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator)
|
||||||
@@ -46,13 +59,12 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat);
|
Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat);
|
||||||
|
|
||||||
_gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger<GameChatHooks>(), gameInteropProvider, SendChatShell));
|
_gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger<GameChatHooks>(), gameInteropProvider, SendChatShell));
|
||||||
|
|
||||||
// Initialize chat hooks in advance
|
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ = _gameChatHooks.Value;
|
_ = _gameChatHooks.Value;
|
||||||
|
_isTypingAnnounced = false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -64,15 +76,87 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
_typingCts?.Cancel();
|
||||||
|
_typingCts?.Dispose();
|
||||||
if (_gameChatHooks.IsValueCreated)
|
if (_gameChatHooks.IsValueCreated)
|
||||||
_gameChatHooks.Value!.Dispose();
|
_gameChatHooks.Value!.Dispose();
|
||||||
}
|
}
|
||||||
|
public void NotifyTypingKeystroke(TypingScope scope)
|
||||||
|
{
|
||||||
|
lock (_typingLock)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (!_isTypingAnnounced || (now - _lastTypingSent) >= TypingResendInterval)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try { await _apiController.UserSetTypingState(true, scope).ConfigureAwait(false); }
|
||||||
|
catch (Exception ex) { _logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=true"); }
|
||||||
|
});
|
||||||
|
_isTypingAnnounced = true;
|
||||||
|
_lastTypingSent = now;
|
||||||
|
_lastScope = scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
_typingCts?.Cancel();
|
||||||
|
_typingCts?.Dispose();
|
||||||
|
_typingCts = new CancellationTokenSource();
|
||||||
|
var token = _typingCts.Token;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(TypingIdle, token).ConfigureAwait(false);
|
||||||
|
await _apiController.UserSetTypingState(false, _lastScope).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// reset timer
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=false");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_typingLock)
|
||||||
|
{
|
||||||
|
if (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_isTypingAnnounced = false;
|
||||||
|
_lastTypingSent = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void ClearTypingState()
|
||||||
|
{
|
||||||
|
lock (_typingLock)
|
||||||
|
{
|
||||||
|
_typingCts?.Cancel();
|
||||||
|
_typingCts?.Dispose();
|
||||||
|
_typingCts = null;
|
||||||
|
if (_isTypingAnnounced)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try { await _apiController.UserSetTypingState(false, _lastScope).ConfigureAwait(false); }
|
||||||
|
catch (Exception ex) { _logger.LogDebug(ex, "ClearTypingState: failed to send typing=false"); }
|
||||||
|
});
|
||||||
|
_isTypingAnnounced = false;
|
||||||
|
_lastTypingSent = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleUserChat(UserChatMsgMessage message)
|
private void HandleUserChat(UserChatMsgMessage message)
|
||||||
{
|
{
|
||||||
var chatMsg = message.ChatMsg;
|
var chatMsg = message.ChatMsg;
|
||||||
var prefix = new SeStringBuilder();
|
var prefix = new SeStringBuilder();
|
||||||
prefix.AddText("[SnowChat] ");
|
prefix.AddText("[UmbraChat] ");
|
||||||
_chatGui.Print(new XivChatEntry{
|
_chatGui.Print(new XivChatEntry{
|
||||||
MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent],
|
MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent],
|
||||||
Name = chatMsg.SenderName,
|
Name = chatMsg.SenderName,
|
||||||
@@ -113,6 +197,10 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
var extraChatTags = _mareConfig.Current.ExtraChatTags;
|
var extraChatTags = _mareConfig.Current.ExtraChatTags;
|
||||||
var logKind = ResolveShellLogKind(shellConfig.LogKind);
|
var logKind = ResolveShellLogKind(shellConfig.LogKind);
|
||||||
|
|
||||||
|
var payload = SeString.Parse(message.ChatMsg.PayloadContent);
|
||||||
|
if (TryHandleManualPairInvite(message, payload))
|
||||||
|
return;
|
||||||
|
|
||||||
var msg = new SeStringBuilder();
|
var msg = new SeStringBuilder();
|
||||||
if (extraChatTags)
|
if (extraChatTags)
|
||||||
{
|
{
|
||||||
@@ -124,7 +212,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
msg.AddText($"[SS{shellNumber}]<");
|
msg.AddText($"[SS{shellNumber}]<");
|
||||||
if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal))
|
if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
// Don't link to your own character
|
|
||||||
msg.AddText(chatMsg.SenderName);
|
msg.AddText(chatMsg.SenderName);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -132,7 +219,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId));
|
msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId));
|
||||||
}
|
}
|
||||||
msg.AddText("> ");
|
msg.AddText("> ");
|
||||||
msg.Append(SeString.Parse(message.ChatMsg.PayloadContent));
|
msg.Append(payload);
|
||||||
if (color != 0)
|
if (color != 0)
|
||||||
msg.AddUiForegroundOff();
|
msg.AddUiForegroundOff();
|
||||||
|
|
||||||
@@ -143,7 +230,51 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print an example message to the configured global chat channel
|
private bool TryHandleManualPairInvite(GroupChatMsgMessage message, SeString payload)
|
||||||
|
{
|
||||||
|
var textValue = payload.TextValue;
|
||||||
|
if (string.IsNullOrEmpty(textValue) || !textValue.StartsWith(ManualPairInvitePrefix, StringComparison.Ordinal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var content = textValue[ManualPairInvitePrefix.Length..];
|
||||||
|
if (content.EndsWith("]", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
content = content[..^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = content.Split('|');
|
||||||
|
if (parts.Length < 4)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var sourceUid = parts[0];
|
||||||
|
var sourceAlias = DecodeInviteField(parts[1]);
|
||||||
|
var targetUid = parts[2];
|
||||||
|
var displayName = DecodeInviteField(parts[3]);
|
||||||
|
var inviteId = parts.Length > 4 ? parts[4] : Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
|
if (!string.Equals(targetUid, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Mediator.Publish(new ManualPairInviteMessage(sourceUid, sourceAlias, targetUid, string.IsNullOrEmpty(displayName) ? null : displayName, inviteId));
|
||||||
|
_logger.LogDebug("Received manual pair invite from {source} via syncshell", sourceUid);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DecodeInviteField(string encoded)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(encoded)) return string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = Convert.FromBase64String(encoded);
|
||||||
|
return Encoding.UTF8.GetString(bytes);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
public void PrintChannelExample(string message, string gid = "")
|
public void PrintChannelExample(string message, string gid = "")
|
||||||
{
|
{
|
||||||
int chatType = _mareConfig.Current.ChatLogKind;
|
int chatType = _mareConfig.Current.ChatLogKind;
|
||||||
@@ -164,8 +295,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
Type = (XivChatType)chatType
|
Type = (XivChatType)chatType
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called to update the active chat shell name if its renamed
|
|
||||||
public void MaybeUpdateShellName(int shellNumber)
|
public void MaybeUpdateShellName(int shellNumber)
|
||||||
{
|
{
|
||||||
if (_mareConfig.Current.DisableSyncshellChat)
|
if (_mareConfig.Current.DisableSyncshellChat)
|
||||||
@@ -178,7 +307,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null)
|
if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null)
|
||||||
{
|
{
|
||||||
// Very dumb and won't handle re-numbering -- need to identify the active chat channel more reliably later
|
|
||||||
if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal))
|
if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal))
|
||||||
SwitchChatShell(shellNumber);
|
SwitchChatShell(shellNumber);
|
||||||
}
|
}
|
||||||
@@ -197,7 +325,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
||||||
{
|
{
|
||||||
var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID;
|
var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID;
|
||||||
// BUG: This doesn't always update the chat window e.g. when renaming a group
|
|
||||||
_gameChatHooks.Value.ChatChannelOverride = new()
|
_gameChatHooks.Value.ChatChannelOverride = new()
|
||||||
{
|
{
|
||||||
ChannelName = $"SS [{shellNumber}]: {name}",
|
ChannelName = $"SS [{shellNumber}]: {name}",
|
||||||
@@ -221,7 +348,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () => {
|
_ = Task.Run(async () => {
|
||||||
// Should cache the name and home world instead of fetching it every time
|
|
||||||
var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => {
|
var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => {
|
||||||
return new ChatMessage()
|
return new ChatMessage()
|
||||||
{
|
{
|
||||||
@@ -230,6 +356,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
PayloadContent = chatBytes
|
PayloadContent = chatBytes
|
||||||
};
|
};
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
ClearTypingState();
|
||||||
await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false);
|
await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
@@ -238,4 +365,4 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
_chatGui.PrintError($"[UmbraSync] Syncshell number #{shellNumber} not found");
|
_chatGui.PrintError($"[UmbraSync] Syncshell number #{shellNumber} not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
68
MareSynchronos/Services/ChatTwoCompatibilityService.cs
Normal file
68
MareSynchronos/Services/ChatTwoCompatibilityService.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class ChatTwoCompatibilityService : MediatorSubscriberBase, IHostedService
|
||||||
|
{
|
||||||
|
private const string ChatTwoInternalName = "ChatTwo";
|
||||||
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
|
private bool _warningShown;
|
||||||
|
|
||||||
|
public ChatTwoCompatibilityService(ILogger<ChatTwoCompatibilityService> logger, IDalamudPluginInterface pluginInterface, MareMediator mediator)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_pluginInterface = pluginInterface;
|
||||||
|
|
||||||
|
Mediator.SubscribeKeyed<PluginChangeMessage>(this, ChatTwoInternalName, OnChatTwoStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var initialState = PluginWatcherService.GetInitialPluginState(_pluginInterface, ChatTwoInternalName);
|
||||||
|
if (initialState?.IsLoaded == true)
|
||||||
|
{
|
||||||
|
ShowWarning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Failed to inspect ChatTwo initial state");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Mediator.UnsubscribeAll(this);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChatTwoStateChanged(PluginChangeMessage message)
|
||||||
|
{
|
||||||
|
if (message.IsLoaded)
|
||||||
|
{
|
||||||
|
ShowWarning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowWarning()
|
||||||
|
{
|
||||||
|
if (_warningShown) return;
|
||||||
|
_warningShown = true;
|
||||||
|
|
||||||
|
const string warningTitle = "ChatTwo détecté";
|
||||||
|
const string warningBody = "Actuellement, le plugin ChatTwo n'est pas compatible avec la bulle d'écriture d'UmbraSync. Désactivez ChatTwo si vous souhaitez conserver l'indicateur de saisie.";
|
||||||
|
|
||||||
|
Mediator.Publish(new NotificationMessage(warningTitle, warningBody, NotificationType.Warning, TimeSpan.FromSeconds(10)));
|
||||||
|
}
|
||||||
|
}
|
||||||
373
MareSynchronos/Services/ChatTypingDetectionService.cs
Normal file
373
MareSynchronos/Services/ChatTypingDetectionService.cs
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class ChatTypingDetectionService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<ChatTypingDetectionService> _logger;
|
||||||
|
private readonly IFramework _framework;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
|
private readonly IGameGui _gameGui;
|
||||||
|
private readonly ChatService _chatService;
|
||||||
|
private readonly TypingIndicatorStateService _typingStateService;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly IPartyList _partyList;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
|
||||||
|
private string _lastChatText = string.Empty;
|
||||||
|
private bool _isTyping;
|
||||||
|
private bool _notifyingRemote;
|
||||||
|
private bool _serverSupportWarnLogged;
|
||||||
|
private bool _remoteNotificationsEnabled;
|
||||||
|
private bool _subscribed;
|
||||||
|
|
||||||
|
public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework,
|
||||||
|
IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList,
|
||||||
|
TypingIndicatorStateService typingStateService, ApiController apiController, MareConfigService configService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_framework = framework;
|
||||||
|
_clientState = clientState;
|
||||||
|
_gameGui = gameGui;
|
||||||
|
_chatService = chatService;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_partyList = partyList;
|
||||||
|
_typingStateService = typingStateService;
|
||||||
|
_apiController = apiController;
|
||||||
|
_configService = configService;
|
||||||
|
|
||||||
|
Subscribe();
|
||||||
|
_logger.LogInformation("ChatTypingDetectionService initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Unsubscribe();
|
||||||
|
ResetTypingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SoftRestart()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("TypingDetection: soft restarting");
|
||||||
|
Unsubscribe();
|
||||||
|
ResetTypingState();
|
||||||
|
_chatService.ClearTypingState();
|
||||||
|
_typingStateService.ClearAll();
|
||||||
|
Subscribe();
|
||||||
|
_logger.LogInformation("TypingDetection: soft restart completed");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "TypingDetection: soft restart failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Subscribe()
|
||||||
|
{
|
||||||
|
if (_subscribed) return;
|
||||||
|
_framework.Update += OnFrameworkUpdate;
|
||||||
|
_subscribed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unsubscribe()
|
||||||
|
{
|
||||||
|
if (!_subscribed) return;
|
||||||
|
_framework.Update -= OnFrameworkUpdate;
|
||||||
|
_subscribed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_clientState.IsLoggedIn)
|
||||||
|
{
|
||||||
|
ResetTypingState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_configService.Current.TypingIndicatorEnabled)
|
||||||
|
{
|
||||||
|
ResetTypingState();
|
||||||
|
_chatService.ClearTypingState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText))
|
||||||
|
{
|
||||||
|
ResetTypingState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsIgnoredCommand(chatText))
|
||||||
|
{
|
||||||
|
ResetTypingState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var notifyRemote = ShouldNotifyRemote();
|
||||||
|
UpdateRemoteNotificationLogState(notifyRemote);
|
||||||
|
if (!notifyRemote && _notifyingRemote)
|
||||||
|
{
|
||||||
|
_chatService.ClearTypingState();
|
||||||
|
_notifyingRemote = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isTyping || !string.Equals(chatText, _lastChatText, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (notifyRemote)
|
||||||
|
{
|
||||||
|
var scope = GetCurrentTypingScope();
|
||||||
|
_chatService.NotifyTypingKeystroke(scope);
|
||||||
|
_notifyingRemote = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_typingStateService.SetSelfTypingLocal(true);
|
||||||
|
_isTyping = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastChatText = chatText;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogTrace(ex, "ChatTypingDetectionService tick failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetTypingState()
|
||||||
|
{
|
||||||
|
if (!_isTyping)
|
||||||
|
{
|
||||||
|
_lastChatText = string.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTyping = false;
|
||||||
|
_lastChatText = string.Empty;
|
||||||
|
_chatService.ClearTypingState();
|
||||||
|
_notifyingRemote = false;
|
||||||
|
_typingStateService.SetSelfTypingLocal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe TypingScope GetCurrentTypingScope()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var shellModule = RaptureShellModule.Instance();
|
||||||
|
if (shellModule == null)
|
||||||
|
return TypingScope.Unknown;
|
||||||
|
|
||||||
|
var chatType = (XivChatType)shellModule->ChatType;
|
||||||
|
switch (chatType)
|
||||||
|
{
|
||||||
|
case XivChatType.Say:
|
||||||
|
case XivChatType.Shout:
|
||||||
|
case XivChatType.Yell:
|
||||||
|
return TypingScope.Proximity;
|
||||||
|
case XivChatType.Party:
|
||||||
|
return TypingScope.Party;
|
||||||
|
case XivChatType.CrossParty:
|
||||||
|
return TypingScope.CrossParty;
|
||||||
|
default:
|
||||||
|
return TypingScope.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return TypingScope.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsIgnoredCommand(string chatText)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(chatText))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var trimmed = chatText.TrimStart();
|
||||||
|
if (!trimmed.StartsWith('/'))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var firstTokenEnd = trimmed.IndexOf(' ');
|
||||||
|
var command = firstTokenEnd >= 0 ? trimmed[..firstTokenEnd] : trimmed;
|
||||||
|
command = command.TrimEnd();
|
||||||
|
|
||||||
|
var comparison = StringComparison.OrdinalIgnoreCase;
|
||||||
|
return command.StartsWith("/tell", comparison)
|
||||||
|
|| command.StartsWith("/t", comparison)
|
||||||
|
|| command.StartsWith("/xllog", comparison)
|
||||||
|
|| command.StartsWith("/umbra", comparison)
|
||||||
|
|| command.StartsWith("/fc", comparison)
|
||||||
|
|| command.StartsWith("/freecompany", comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool ShouldNotifyRemote()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_configService.Current.TypingIndicatorEnabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState;
|
||||||
|
var connected = _apiController.IsConnected;
|
||||||
|
if (!connected || !supportsTypingState)
|
||||||
|
{
|
||||||
|
if (!_serverSupportWarnLogged)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("TypingDetection: server support unavailable (connected={connected}, supports={supports})", connected, supportsTypingState);
|
||||||
|
_serverSupportWarnLogged = true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_serverSupportWarnLogged = false;
|
||||||
|
|
||||||
|
var shellModule = RaptureShellModule.Instance();
|
||||||
|
if (shellModule == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("TypingDetection: shell module null");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatType = (XivChatType)shellModule->ChatType;
|
||||||
|
switch (chatType)
|
||||||
|
{
|
||||||
|
case XivChatType.Say:
|
||||||
|
case XivChatType.Shout:
|
||||||
|
case XivChatType.Yell:
|
||||||
|
return true;
|
||||||
|
case XivChatType.Party:
|
||||||
|
case XivChatType.CrossParty:
|
||||||
|
var eligible = PartyContainsPairedMember();
|
||||||
|
return eligible;
|
||||||
|
case XivChatType.Debug:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
_logger.LogTrace("TypingDetection: channel {type} rejected", chatType);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogTrace(ex, "ChatTypingDetectionService: failed to evaluate chat channel");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PartyContainsPairedMember()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pairedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var pair in _pairManager.GetOnlineUserPairs())
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(pair.PlayerName))
|
||||||
|
pairedNames.Add(pair.PlayerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pairedNames.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("TypingDetection: no paired names online");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var member in _partyList)
|
||||||
|
{
|
||||||
|
var name = member?.Name?.TextValue;
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (pairedNames.Contains(name))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "ChatTypingDetectionService: failed to check party composition");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("TypingDetection: no paired members in party");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private unsafe bool TryGetChatInput(out string chatText)
|
||||||
|
{
|
||||||
|
chatText = string.Empty;
|
||||||
|
|
||||||
|
var addon = _gameGui.GetAddonByName("ChatLog", 1);
|
||||||
|
if (addon.Address == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var chatLog = (AtkUnitBase*)addon.Address;
|
||||||
|
if (chatLog == null || !chatLog->IsVisible)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var textInputNode = chatLog->UldManager.NodeList[16];
|
||||||
|
if (textInputNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var componentNode = textInputNode->GetAsAtkComponentNode();
|
||||||
|
if (componentNode == null || componentNode->Component == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var cursorNode = componentNode->Component->UldManager.NodeList[14];
|
||||||
|
if (cursorNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var cursorVisible = cursorNode->IsVisible();
|
||||||
|
if (!cursorVisible)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatInputNode = componentNode->Component->UldManager.NodeList[1];
|
||||||
|
if (chatInputNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var textNode = chatInputNode->GetAsAtkTextNode();
|
||||||
|
if (textNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var rawText = textNode->GetText();
|
||||||
|
if (rawText == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
chatText = rawText.AsDalamudSeString().ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateRemoteNotificationLogState(bool notifyRemote)
|
||||||
|
{
|
||||||
|
if (notifyRemote && !_remoteNotificationsEnabled)
|
||||||
|
{
|
||||||
|
_remoteNotificationsEnabled = true;
|
||||||
|
_logger.LogInformation("TypingDetection: remote notifications enabled");
|
||||||
|
}
|
||||||
|
else if (!notifyRemote && _remoteNotificationsEnabled)
|
||||||
|
{
|
||||||
|
_remoteNotificationsEnabled = false;
|
||||||
|
_logger.LogInformation("TypingDetection: remote notifications disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,15 +9,15 @@ using MareSynchronos.UI;
|
|||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
public sealed class CommandManagerService : IDisposable
|
public sealed class CommandManagerService : IDisposable
|
||||||
{
|
{
|
||||||
private const string _commandName = "/sync";
|
private const string _commandName = "/usync";
|
||||||
private const string _commandName2 = "/umbra";
|
private const string _autoDetectCommand = "/autodetect";
|
||||||
|
private const string _ssCommandPrefix = "/ums";
|
||||||
private const string _ssCommandPrefix = "/ss";
|
|
||||||
|
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly ICommandManager _commandManager;
|
private readonly ICommandManager _commandManager;
|
||||||
@@ -42,11 +42,12 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
_mareConfigService = mareConfigService;
|
_mareConfigService = mareConfigService;
|
||||||
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
|
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
|
||||||
{
|
{
|
||||||
HelpMessage = "Opens the Umbra UI"
|
HelpMessage = "Opens the UmbraSync UI"
|
||||||
});
|
});
|
||||||
_commandManager.AddHandler(_commandName2, new CommandInfo(OnCommand)
|
|
||||||
|
_commandManager.AddHandler(_autoDetectCommand, new CommandInfo(OnAutoDetectCommand)
|
||||||
{
|
{
|
||||||
HelpMessage = "Opens the Umbra UI"
|
HelpMessage = "Opens the AutoDetect window"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
|
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
|
||||||
@@ -62,12 +63,21 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_commandManager.RemoveHandler(_commandName);
|
_commandManager.RemoveHandler(_commandName);
|
||||||
_commandManager.RemoveHandler(_commandName2);
|
_commandManager.RemoveHandler(_autoDetectCommand);
|
||||||
|
|
||||||
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
||||||
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
|
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnAutoDetectCommand(string command, string args)
|
||||||
|
{
|
||||||
|
UiSharedService.AccentColor = new Vector4(0x8D / 255f, 0x37 / 255f, 0xC0 / 255f, 1f);
|
||||||
|
UiSharedService.AccentHoverColor = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f);
|
||||||
|
UiSharedService.AccentActiveColor = UiSharedService.AccentHoverColor;
|
||||||
|
_mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void OnCommand(string command, string args)
|
private void OnCommand(string command, string args)
|
||||||
{
|
{
|
||||||
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
|
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
|
||||||
@@ -147,9 +157,8 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// FIXME: Chat content seems to already be stripped of any special characters here?
|
|
||||||
byte[] chatBytes = Encoding.UTF8.GetBytes(args);
|
byte[] chatBytes = Encoding.UTF8.GetBytes(args);
|
||||||
_chatService.SendChatShell(shellNumber, chatBytes);
|
_chatService.SendChatShell(shellNumber, chatBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||||
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject;
|
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject;
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
private readonly ILogger<DalamudUtilService> _logger;
|
private readonly ILogger<DalamudUtilService> _logger;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
|
private readonly Dictionary<string, ConditionFlag> _conditionLookup = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private uint? _classJobId = 0;
|
private uint? _classJobId = 0;
|
||||||
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||||
private string _lastGlobalBlockPlayer = string.Empty;
|
private string _lastGlobalBlockPlayer = string.Empty;
|
||||||
@@ -172,6 +175,20 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
public bool IsInCombatOrPerforming { get; private set; } = false;
|
public bool IsInCombatOrPerforming { get; private set; } = false;
|
||||||
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
||||||
|
|
||||||
|
public bool IsConditionActive(string flagName)
|
||||||
|
{
|
||||||
|
if (_conditionLookup.TryGetValue(flagName, out var cachedFlag))
|
||||||
|
return _condition[cachedFlag];
|
||||||
|
|
||||||
|
if (Enum.TryParse<ConditionFlag>(flagName, true, out var flag))
|
||||||
|
{
|
||||||
|
_conditionLookup[flagName] = flag;
|
||||||
|
return _condition[flag];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
||||||
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; }
|
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; }
|
||||||
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
|
using System;
|
||||||
using Dalamud.Game.Gui.NamePlate;
|
using Dalamud.Game.Gui.NamePlate;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
public class GuiHookService : DisposableMediatorSubscriberBase
|
public class GuiHookService : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly ILogger<GuiHookService> _logger;
|
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly MareConfigService _configService;
|
private readonly MareConfigService _configService;
|
||||||
private readonly INamePlateGui _namePlateGui;
|
private readonly INamePlateGui _namePlateGui;
|
||||||
@@ -27,7 +30,6 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
|||||||
INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList, PairManager pairManager)
|
INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList, PairManager pairManager)
|
||||||
: base(logger, mediator)
|
: base(logger, mediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_namePlateGui = namePlateGui;
|
_namePlateGui = namePlateGui;
|
||||||
@@ -41,11 +43,14 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => GameSettingsCheck());
|
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => GameSettingsCheck());
|
||||||
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw());
|
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw());
|
||||||
Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw());
|
Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw());
|
||||||
|
Mediator.Subscribe<UserTypingStateMessage>(this, (_) => RequestRedraw());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RequestRedraw(bool force = false)
|
public void RequestRedraw(bool force = false)
|
||||||
{
|
{
|
||||||
if (!_configService.Current.UseNameColors)
|
var useColors = _configService.Current.UseNameColors;
|
||||||
|
|
||||||
|
if (!useColors)
|
||||||
{
|
{
|
||||||
if (!_isModified && !force)
|
if (!_isModified && !force)
|
||||||
return;
|
return;
|
||||||
@@ -69,7 +74,8 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||||
{
|
{
|
||||||
if (!_configService.Current.UseNameColors)
|
var applyColors = _configService.Current.UseNameColors;
|
||||||
|
if (!applyColors)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue);
|
var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue);
|
||||||
@@ -89,13 +95,18 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
|||||||
if (_namePlateRoleColorsEnabled && partyMembers.Contains(handler.GameObject?.Address ?? nint.MaxValue))
|
if (_namePlateRoleColorsEnabled && partyMembers.Contains(handler.GameObject?.Address ?? nint.MaxValue))
|
||||||
continue;
|
continue;
|
||||||
var pair = visibleUsersDict[handler.GameObjectId];
|
var pair = visibleUsersDict[handler.GameObjectId];
|
||||||
var colors = !pair.IsApplicationBlocked ? _configService.Current.NameColors : _configService.Current.BlockedNameColors;
|
if (applyColors)
|
||||||
handler.NameParts.TextWrap = (
|
{
|
||||||
BuildColorStartSeString(colors),
|
var colors = !pair.IsApplicationBlocked ? _configService.Current.NameColors : _configService.Current.BlockedNameColors;
|
||||||
BuildColorEndSeString(colors)
|
handler.NameParts.TextWrap = (
|
||||||
);
|
BuildColorStartSeString(colors),
|
||||||
_isModified = true;
|
BuildColorEndSeString(colors)
|
||||||
|
);
|
||||||
|
_isModified = true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
using MareSynchronos.API.Data.Comparer;
|
using MareSynchronos.API.Data.Comparer;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -15,7 +14,6 @@ public class MareProfileManager : MediatorSubscriberBase
|
|||||||
private const string _nsfw = "Profile not displayed - NSFW";
|
private const string _nsfw = "Profile not displayed - NSFW";
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly MareConfigService _mareConfigService;
|
private readonly MareConfigService _mareConfigService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
|
||||||
private readonly ConcurrentDictionary<UserData, MareProfileData> _mareProfiles = new(UserDataComparer.Instance);
|
private readonly ConcurrentDictionary<UserData, MareProfileData> _mareProfiles = new(UserDataComparer.Instance);
|
||||||
|
|
||||||
private readonly MareProfileData _defaultProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _noDescription);
|
private readonly MareProfileData _defaultProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _noDescription);
|
||||||
@@ -23,11 +21,10 @@ public class MareProfileManager : MediatorSubscriberBase
|
|||||||
private readonly MareProfileData _nsfwProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _nsfw);
|
private readonly MareProfileData _nsfwProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _nsfw);
|
||||||
|
|
||||||
public MareProfileManager(ILogger<MareProfileManager> logger, MareConfigService mareConfigService,
|
public MareProfileManager(ILogger<MareProfileManager> logger, MareConfigService mareConfigService,
|
||||||
MareMediator mediator, ApiController apiController, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator)
|
MareMediator mediator, ApiController apiController) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_mareConfigService = mareConfigService;
|
_mareConfigService = mareConfigService;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
|
|
||||||
Mediator.Subscribe<ClearProfileDataMessage>(this, (msg) =>
|
Mediator.Subscribe<ClearProfileDataMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
@@ -75,4 +72,4 @@ public class MareProfileManager : MediatorSubscriberBase
|
|||||||
_mareProfiles[data] = _defaultProfileData;
|
_mareProfiles[data] = _defaultProfileData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using System.Text;
|
|||||||
|
|
||||||
namespace MareSynchronos.Services.Mediator;
|
namespace MareSynchronos.Services.Mediator;
|
||||||
|
|
||||||
public sealed class MareMediator : IHostedService
|
public sealed class MareMediator : IHostedService, IDisposable
|
||||||
{
|
{
|
||||||
private readonly Lock _addRemoveLock = new();
|
private readonly Lock _addRemoveLock = new();
|
||||||
private readonly ConcurrentDictionary<SubscriberAction, DateTime> _lastErrorTime = [];
|
private readonly ConcurrentDictionary<SubscriberAction, DateTime> _lastErrorTime = [];
|
||||||
@@ -109,6 +109,26 @@ public sealed class MareMediator : IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
if (!_loopCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_loopCts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// already disposed, swallow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_loopCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
public void SubscribeKeyed<T>(IMediatorSubscriber subscriber, string key, Action<T> action) where T : MessageBase
|
public void SubscribeKeyed<T>(IMediatorSubscriber subscriber, string key, Action<T> action) where T : MessageBase
|
||||||
{
|
{
|
||||||
lock (_addRemoveLock)
|
lock (_addRemoveLock)
|
||||||
@@ -219,4 +239,4 @@ public sealed class MareMediator : IHostedService
|
|||||||
public object Action { get; }
|
public object Action { get; }
|
||||||
public IMediatorSubscriber Subscriber { get; }
|
public IMediatorSubscriber Subscriber { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using MareSynchronos.API.Data;
|
|||||||
using MareSynchronos.API.Dto;
|
using MareSynchronos.API.Dto;
|
||||||
using MareSynchronos.API.Dto.CharaData;
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.PlayerData.Handlers;
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
@@ -12,7 +13,7 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace MareSynchronos.Services.Mediator;
|
namespace MareSynchronos.Services.Mediator;
|
||||||
|
|
||||||
#pragma warning disable MA0048 // File name must match type name
|
#pragma warning disable MA0048
|
||||||
#pragma warning disable S2094
|
#pragma warning disable S2094
|
||||||
public record SwitchToIntroUiMessage : MessageBase;
|
public record SwitchToIntroUiMessage : MessageBase;
|
||||||
public record SwitchToMainUiMessage : MessageBase;
|
public record SwitchToMainUiMessage : MessageBase;
|
||||||
@@ -52,6 +53,7 @@ public record HaltScanMessage(string Source) : MessageBase;
|
|||||||
public record ResumeScanMessage(string Source) : MessageBase;
|
public record ResumeScanMessage(string Source) : MessageBase;
|
||||||
public record NotificationMessage
|
public record NotificationMessage
|
||||||
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
||||||
|
public record DualNotificationMessage(string Title, string Message, NotificationType Type, TimeSpan? ToastDuration = null) : MessageBase;
|
||||||
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
||||||
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
||||||
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
||||||
@@ -90,6 +92,7 @@ public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBas
|
|||||||
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
|
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
|
||||||
public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase;
|
public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase;
|
||||||
public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase;
|
public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase;
|
||||||
|
public record UserTypingStateMessage(TypingStateDto Typing) : MessageBase;
|
||||||
public record RecalculatePerformanceMessage(string? UID) : MessageBase;
|
public record RecalculatePerformanceMessage(string? UID) : MessageBase;
|
||||||
public record NameplateRedrawMessage : MessageBase;
|
public record NameplateRedrawMessage : MessageBase;
|
||||||
public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
|
public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
|
||||||
@@ -108,6 +111,20 @@ public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadD
|
|||||||
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
||||||
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||||
|
|
||||||
|
public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMatch, string? Token, string? DisplayName, string? Uid, bool AcceptPairRequests = true);
|
||||||
|
public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
|
||||||
|
public record NearbyDetectionToggled(bool Enabled) : MessageBase;
|
||||||
|
public record AllowPairRequestsToggled(bool Enabled) : MessageBase;
|
||||||
|
public record SyncshellDiscoveryUpdated(List<SyncshellDiscoveryEntryDto> Entries) : MessageBase;
|
||||||
|
public record SyncshellAutoDetectStateChanged(string Gid, bool Visible, bool PasswordTemporarilyDisabled) : MessageBase;
|
||||||
|
public record ManualPairInviteMessage(string SourceUid, string SourceAlias, string TargetUid, string? DisplayName, string InviteId) : MessageBase;
|
||||||
|
public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
|
||||||
|
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase;
|
||||||
|
public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : MessageBase;
|
||||||
|
public record PairSyncOverrideChanged(string Uid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
||||||
|
public record GroupSyncOverrideChanged(string Gid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
||||||
|
public record NotificationStateChanged(int TotalCount) : MessageBase;
|
||||||
|
|
||||||
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
|
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
|
||||||
#pragma warning restore S2094
|
#pragma warning restore S2094
|
||||||
#pragma warning restore MA0048 // File name must match type name
|
#pragma warning restore MA0048
|
||||||
|
|||||||
144
MareSynchronos/Services/Notification/NotificationTracker.cs
Normal file
144
MareSynchronos/Services/Notification/NotificationTracker.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.Notifications;
|
||||||
|
|
||||||
|
public enum NotificationCategory
|
||||||
|
{
|
||||||
|
AutoDetect,
|
||||||
|
Syncshell,
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record NotificationEntry(NotificationCategory Category, string Id, string Title, string? Description, DateTime CreatedAt)
|
||||||
|
{
|
||||||
|
public static NotificationEntry AutoDetect(string uid, string displayName)
|
||||||
|
=> new(NotificationCategory.AutoDetect, uid, displayName, "Nouvelle demande d'appairage via AutoDetect.", DateTime.UtcNow);
|
||||||
|
|
||||||
|
public static NotificationEntry SyncshellPublic(string gid, string aliasOrGid)
|
||||||
|
=> new(NotificationCategory.Syncshell, gid, $"Syncshell publique: {aliasOrGid}", "La Syncshell est désormais visible via AutoDetect.", DateTime.UtcNow);
|
||||||
|
|
||||||
|
public static NotificationEntry SyncshellNotPublic(string gid, string aliasOrGid)
|
||||||
|
=> new(NotificationCategory.Syncshell, gid, $"Syncshell non publique: {aliasOrGid}", "La Syncshell n'est plus visible via AutoDetect.", DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class NotificationTracker
|
||||||
|
{
|
||||||
|
private const int MaxStored = 100;
|
||||||
|
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly NotificationsConfigService _configService;
|
||||||
|
private readonly Dictionary<(NotificationCategory Category, string Id), NotificationEntry> _entries = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
public NotificationTracker(MareMediator mediator, NotificationsConfigService configService)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
_configService = configService;
|
||||||
|
LoadPersisted();
|
||||||
|
PublishState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Upsert(NotificationEntry entry)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_entries[(entry.Category, entry.Id)] = entry;
|
||||||
|
TrimIfNecessary_NoLock();
|
||||||
|
Persist_NoLock();
|
||||||
|
}
|
||||||
|
PublishState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(NotificationCategory category, string id)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_entries.Remove((category, id));
|
||||||
|
Persist_NoLock();
|
||||||
|
}
|
||||||
|
PublishState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<NotificationEntry> GetEntries()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _entries.Values
|
||||||
|
.OrderBy(e => e.CreatedAt)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _entries.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishState()
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationStateChanged(Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadPersisted()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = _configService.Current.Notifications ?? new List<StoredNotification>();
|
||||||
|
foreach (var s in list)
|
||||||
|
{
|
||||||
|
if (!Enum.TryParse<NotificationCategory>(s.Category, out var cat)) continue;
|
||||||
|
var entry = new NotificationEntry(cat, s.Id, s.Title, s.Description, s.CreatedAtUtc);
|
||||||
|
_entries[(entry.Category, entry.Id)] = entry;
|
||||||
|
}
|
||||||
|
TrimIfNecessary_NoLock();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore load errors, start empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Persist_NoLock()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stored = _entries.Values
|
||||||
|
.OrderBy(e => e.CreatedAt)
|
||||||
|
.Select(e => new StoredNotification
|
||||||
|
{
|
||||||
|
Category = e.Category.ToString(),
|
||||||
|
Id = e.Id,
|
||||||
|
Title = e.Title,
|
||||||
|
Description = e.Description,
|
||||||
|
CreatedAtUtc = e.CreatedAt
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
_configService.Current.Notifications = stored;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore persistence errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrimIfNecessary_NoLock()
|
||||||
|
{
|
||||||
|
if (_entries.Count <= MaxStored) return;
|
||||||
|
foreach (var kv in _entries.Values.OrderByDescending(v => v.CreatedAt).Skip(MaxStored).ToList())
|
||||||
|
{
|
||||||
|
_entries.Remove((kv.Category, kv.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using Dalamud.Game.Text.SeStringHandling;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
@@ -16,21 +18,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
private readonly INotificationManager _notificationManager;
|
private readonly INotificationManager _notificationManager;
|
||||||
private readonly IChatGui _chatGui;
|
private readonly IChatGui _chatGui;
|
||||||
private readonly MareConfigService _configurationService;
|
private readonly MareConfigService _configurationService;
|
||||||
|
private readonly Services.Notifications.NotificationTracker _notificationTracker;
|
||||||
|
private readonly PlayerData.Pairs.PairManager _pairManager;
|
||||||
|
|
||||||
public NotificationService(ILogger<NotificationService> logger, MareMediator mediator,
|
public NotificationService(ILogger<NotificationService> logger, MareMediator mediator,
|
||||||
DalamudUtilService dalamudUtilService,
|
DalamudUtilService dalamudUtilService,
|
||||||
INotificationManager notificationManager,
|
INotificationManager notificationManager,
|
||||||
IChatGui chatGui, MareConfigService configurationService) : base(logger, mediator)
|
IChatGui chatGui, MareConfigService configurationService,
|
||||||
|
Services.Notifications.NotificationTracker notificationTracker,
|
||||||
|
PlayerData.Pairs.PairManager pairManager) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_notificationManager = notificationManager;
|
_notificationManager = notificationManager;
|
||||||
_chatGui = chatGui;
|
_chatGui = chatGui;
|
||||||
_configurationService = configurationService;
|
_configurationService = configurationService;
|
||||||
|
_notificationTracker = notificationTracker;
|
||||||
|
_pairManager = pairManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
||||||
|
Mediator.Subscribe<DualNotificationMessage>(this, ShowDualNotification);
|
||||||
|
Mediator.Subscribe<Services.Mediator.SyncshellAutoDetectStateChanged>(this, OnSyncshellAutoDetectStateChanged);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,41 +91,128 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
|
|
||||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||||
|
|
||||||
switch (msg.Type)
|
bool appendInstruction;
|
||||||
|
bool forceChat = ShouldForceChat(msg, out appendInstruction);
|
||||||
|
var effectiveMessage = forceChat && appendInstruction ? AppendAutoDetectInstruction(msg.Message) : msg.Message;
|
||||||
|
var adjustedMsg = forceChat && appendInstruction ? msg with { Message = effectiveMessage } : msg;
|
||||||
|
|
||||||
|
switch (adjustedMsg.Type)
|
||||||
{
|
{
|
||||||
case NotificationType.Info:
|
case NotificationType.Info:
|
||||||
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
|
ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.InfoNotification, forceChat);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NotificationType.Warning:
|
case NotificationType.Warning:
|
||||||
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
|
ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.WarningNotification, forceChat);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NotificationType.Error:
|
case NotificationType.Error:
|
||||||
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
|
ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.ErrorNotification, forceChat);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
private void ShowDualNotification(DualNotificationMessage message)
|
||||||
{
|
{
|
||||||
switch (location)
|
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||||
|
|
||||||
|
var baseMsg = new NotificationMessage(message.Title, message.Message, message.Type, message.ToastDuration);
|
||||||
|
ShowToast(baseMsg);
|
||||||
|
ShowChat(baseMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSyncshellAutoDetectStateChanged(SyncshellAutoDetectStateChanged msg)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
case NotificationLocation.Toast:
|
if (msg.Visible) return; // only handle transition to not visible
|
||||||
ShowToast(msg);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotificationLocation.Chat:
|
var gid = msg.Gid;
|
||||||
ShowChat(msg);
|
// Try to resolve alias from PairManager snapshot; fallback to gid
|
||||||
break;
|
var alias = _pairManager.Groups.Values.FirstOrDefault(g => string.Equals(g.GID, gid, StringComparison.OrdinalIgnoreCase))?.GroupAliasOrGID ?? gid;
|
||||||
|
|
||||||
case NotificationLocation.Both:
|
var title = $"Syncshell non publique: {alias}";
|
||||||
ShowToast(msg);
|
var message = "La Syncshell n'est plus visible via AutoDetect.";
|
||||||
ShowChat(msg);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotificationLocation.Nowhere:
|
// Show toast + chat
|
||||||
break;
|
ShowDualNotification(new DualNotificationMessage(title, message, NotificationType.Info, TimeSpan.FromSeconds(4)));
|
||||||
|
|
||||||
|
// Persist into notification center
|
||||||
|
_notificationTracker.Upsert(Services.Notifications.NotificationEntry.SyncshellNotPublic(gid, alias));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldForceChat(NotificationMessage msg, out bool appendInstruction)
|
||||||
|
{
|
||||||
|
appendInstruction = false;
|
||||||
|
|
||||||
|
bool IsNearbyRequestText(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return false;
|
||||||
|
return text.Contains("Nearby request", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| text.Contains("Nearby Request", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsNearbyAcceptText(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return false;
|
||||||
|
return text.Contains("Nearby Accept", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAccept = IsNearbyAcceptText(msg.Title) || IsNearbyAcceptText(msg.Message);
|
||||||
|
if (isAccept)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool isRequest = IsNearbyRequestText(msg.Title) || IsNearbyRequestText(msg.Message);
|
||||||
|
if (isRequest)
|
||||||
|
{
|
||||||
|
appendInstruction = !IsRequestSentConfirmation(msg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRequestSentConfirmation(NotificationMessage msg)
|
||||||
|
{
|
||||||
|
if (string.Equals(msg.Title, "Nearby request sent", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(msg.Message) && msg.Message.Contains("The other user will receive a request notification.", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string AppendAutoDetectInstruction(string? message)
|
||||||
|
{
|
||||||
|
const string suffix = " | Ouvrez /autodetect pour gérer l'invitation.";
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
return suffix.TrimStart(' ', '|');
|
||||||
|
|
||||||
|
if (message.Contains("/autodetect", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return message;
|
||||||
|
|
||||||
|
return message.TrimEnd() + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location, bool forceChat)
|
||||||
|
{
|
||||||
|
bool showToast = location is NotificationLocation.Toast or NotificationLocation.Both;
|
||||||
|
bool showChat = forceChat || location is NotificationLocation.Chat or NotificationLocation.Both;
|
||||||
|
|
||||||
|
if (showToast)
|
||||||
|
{
|
||||||
|
ShowToast(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showChat)
|
||||||
|
{
|
||||||
|
ShowChat(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,4 +235,4 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.API.Data;
|
using System;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.API.Data.Enum;
|
using MareSynchronos.API.Data.Enum;
|
||||||
using MareSynchronos.FileCache;
|
using MareSynchronos.FileCache;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
@@ -14,7 +15,7 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
||||||
private CancellationTokenSource? _analysisCts;
|
private CancellationTokenSource? _analysisCts;
|
||||||
private CancellationTokenSource _baseAnalysisCts = new();
|
private CancellationTokenSource? _baseAnalysisCts = new();
|
||||||
private string _lastDataHash = string.Empty;
|
private string _lastDataHash = string.Empty;
|
||||||
|
|
||||||
public PairAnalyzer(ILogger<PairAnalyzer> logger, Pair pair, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
public PairAnalyzer(ILogger<PairAnalyzer> logger, Pair pair, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
||||||
@@ -24,8 +25,8 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
Mediator.SubscribeKeyed<PairDataAppliedMessage>(this, pair.UserData.UID, (msg) =>
|
Mediator.SubscribeKeyed<PairDataAppliedMessage>(this, pair.UserData.UID, (msg) =>
|
||||||
{
|
{
|
||||||
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
|
var tokenSource = EnsureFreshCts(ref _baseAnalysisCts);
|
||||||
var token = _baseAnalysisCts.Token;
|
var token = tokenSource.Token;
|
||||||
if (msg.CharacterData != null)
|
if (msg.CharacterData != null)
|
||||||
{
|
{
|
||||||
_ = BaseAnalysis(msg.CharacterData, token);
|
_ = BaseAnalysis(msg.CharacterData, token);
|
||||||
@@ -56,17 +57,15 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void CancelAnalyze()
|
public void CancelAnalyze()
|
||||||
{
|
{
|
||||||
_analysisCts?.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_analysisCts = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
|
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("=== Calculating Character Analysis ===");
|
Logger.LogDebug("=== Calculating Character Analysis ===");
|
||||||
|
|
||||||
_analysisCts = _analysisCts?.CancelRecreate() ?? new();
|
var analysisCts = EnsureFreshCts(ref _analysisCts);
|
||||||
|
var cancelToken = analysisCts.Token;
|
||||||
var cancelToken = _analysisCts.Token;
|
|
||||||
|
|
||||||
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
|
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
|
||||||
if (allFiles.Exists(c => !c.IsComputed || recalculate))
|
if (allFiles.Exists(c => !c.IsComputed || recalculate))
|
||||||
@@ -102,8 +101,7 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
LastPlayerName = Pair.PlayerName ?? string.Empty;
|
LastPlayerName = Pair.PlayerName ?? string.Empty;
|
||||||
Mediator.Publish(new PairDataAnalyzedMessage(Pair.UserData.UID));
|
Mediator.Publish(new PairDataAnalyzedMessage(Pair.UserData.UID));
|
||||||
|
|
||||||
_analysisCts.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_analysisCts = null;
|
|
||||||
|
|
||||||
if (print) PrintAnalysis();
|
if (print) PrintAnalysis();
|
||||||
}
|
}
|
||||||
@@ -114,8 +112,8 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!disposing) return;
|
if (!disposing) return;
|
||||||
|
|
||||||
_analysisCts?.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_baseAnalysisCts.CancelDispose();
|
CancelAndDispose(ref _baseAnalysisCts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||||
@@ -211,4 +209,26 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.OriginalSize))),
|
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.OriginalSize))),
|
||||||
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
|
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
77
MareSynchronos/Services/PartyListTypingService.cs
Normal file
77
MareSynchronos/Services/PartyListTypingService.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public class PartyListTypingService : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<PartyListTypingService> _logger;
|
||||||
|
private readonly IPartyList _partyList;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly TypingIndicatorStateService _typingStateService;
|
||||||
|
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
|
||||||
|
private static readonly TimeSpan TypingDisplayFade = TypingDisplayTime;
|
||||||
|
|
||||||
|
public PartyListTypingService(ILogger<PartyListTypingService> logger,
|
||||||
|
MareMediator mediator,
|
||||||
|
IPartyList partyList,
|
||||||
|
PairManager pairManager,
|
||||||
|
MareConfigService configService,
|
||||||
|
TypingIndicatorStateService typingStateService)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_partyList = partyList;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_configService = configService;
|
||||||
|
_typingStateService = typingStateService;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw()
|
||||||
|
{
|
||||||
|
if (!_configService.Current.TypingIndicatorEnabled) return;
|
||||||
|
if (!_configService.Current.TypingIndicatorShowOnPartyList) return;
|
||||||
|
var visibleByAlias = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var visibleUsers = _pairManager.GetVisibleUsers();
|
||||||
|
foreach (var u in visibleUsers)
|
||||||
|
{
|
||||||
|
var alias = string.IsNullOrEmpty(u.AliasOrUID) ? u.UID : u.AliasOrUID;
|
||||||
|
if (!visibleByAlias.ContainsKey(alias)) visibleByAlias[alias] = u.UID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "PartyListTypingService: failed to get visible users");
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeTypers = _typingStateService.GetActiveTypers(TypingDisplayTime);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
foreach (var member in _partyList)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(member.Name?.TextValue)) continue;
|
||||||
|
|
||||||
|
var displayName = member.Name.TextValue;
|
||||||
|
if (visibleByAlias.TryGetValue(displayName, out var uid)
|
||||||
|
&& activeTypers.TryGetValue(uid, out var entry)
|
||||||
|
&& (now - entry.LastUpdate) <= TypingDisplayFade)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("PartyListTypingService: bubble would be shown for {name}", displayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,6 @@ public class PlayerPerformanceService : DisposableMediatorSubscriberBase
|
|||||||
private readonly MareMediator _mediator;
|
private readonly MareMediator _mediator;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, MareMediator mediator,
|
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, MareMediator mediator,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
@@ -327,4 +326,4 @@ public class PlayerPerformanceService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
return shrunken;
|
return shrunken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
358
MareSynchronos/Services/SyncDefaultsService.cs
Normal file
358
MareSynchronos/Services/SyncDefaultsService.cs
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.API.Data.Extensions;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class SyncDefaultsService : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
|
||||||
|
public SyncDefaultsService(ILogger<SyncDefaultsService> logger, MareMediator mediator,
|
||||||
|
MareConfigService configService, ApiController apiController, PairManager pairManager) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
_apiController = apiController;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
|
||||||
|
Mediator.Subscribe<ApplyDefaultPairPermissionsMessage>(this, OnApplyPairDefaults);
|
||||||
|
Mediator.Subscribe<ApplyDefaultGroupPermissionsMessage>(this, OnApplyGroupDefaults);
|
||||||
|
Mediator.Subscribe<ApplyDefaultsToAllSyncsMessage>(this, msg => ApplyDefaultsToAll(msg));
|
||||||
|
Mediator.Subscribe<PairSyncOverrideChanged>(this, OnPairOverrideChanged);
|
||||||
|
Mediator.Subscribe<GroupSyncOverrideChanged>(this, OnGroupOverrideChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnApplyPairDefaults(ApplyDefaultPairPermissionsMessage message)
|
||||||
|
{
|
||||||
|
var config = _configService.Current;
|
||||||
|
var permissions = message.Pair.OwnPermissions;
|
||||||
|
var overrides = TryGetPairOverride(message.Pair.User.UID);
|
||||||
|
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(message.Pair.User, permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnApplyGroupDefaults(ApplyDefaultGroupPermissionsMessage message)
|
||||||
|
{
|
||||||
|
if (!string.Equals(message.GroupPair.User.UID, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var config = _configService.Current;
|
||||||
|
var permissions = message.GroupPair.GroupUserPermissions;
|
||||||
|
var overrides = TryGetGroupOverride(message.GroupPair.Group.GID);
|
||||||
|
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(message.GroupPair.Group, message.GroupPair.User, permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyDefaultsToAllAsync(ApplyDefaultsToAllSyncsMessage message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var config = _configService.Current;
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
int updatedPairs = 0;
|
||||||
|
int updatedGroups = 0;
|
||||||
|
|
||||||
|
foreach (var pair in _pairManager.DirectPairs.Where(p => p.UserPair != null).ToList())
|
||||||
|
{
|
||||||
|
var permissions = pair.UserPair!.OwnPermissions;
|
||||||
|
var overrides = TryGetPairOverride(pair.UserData.UID);
|
||||||
|
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
updatedPairs++;
|
||||||
|
tasks.Add(_apiController.UserSetPairPermissions(new UserPermissionsDto(pair.UserData, permissions)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var selfUser = new UserData(_apiController.UID);
|
||||||
|
foreach (var groupInfo in _pairManager.Groups.Values.ToList())
|
||||||
|
{
|
||||||
|
var permissions = groupInfo.GroupUserPermissions;
|
||||||
|
var overrides = TryGetGroupOverride(groupInfo.Group.GID);
|
||||||
|
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
updatedGroups++;
|
||||||
|
tasks.Add(_apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(groupInfo.Group, selfUser, permissions)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.Count > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed applying default sync settings to all pairs/groups");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = BuildSummaryMessage(updatedPairs, updatedGroups);
|
||||||
|
var primary = BuildPrimaryMessage(message);
|
||||||
|
var combined = string.IsNullOrEmpty(primary) ? summary : string.Concat(primary, ' ', summary);
|
||||||
|
Mediator.Publish(new DualNotificationMessage("Préférences appliquées", combined, NotificationType.Info));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Unexpected error while applying default sync settings to all pairs/groups");
|
||||||
|
Mediator.Publish(new DualNotificationMessage("Préférences appliquées", "Une erreur est survenue lors de l'application des paramètres par défaut.", NotificationType.Error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyDefaultsToAll(ApplyDefaultsToAllSyncsMessage message) => _ = ApplyDefaultsToAllAsync(message);
|
||||||
|
|
||||||
|
private static string? BuildPrimaryMessage(ApplyDefaultsToAllSyncsMessage message)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(message.Context) || message.Disabled == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var state = message.Disabled.Value ? "désactivée" : "activée";
|
||||||
|
return $"Synchronisation {message.Context} par défaut {state}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSummaryMessage(int pairs, int groups)
|
||||||
|
{
|
||||||
|
if (pairs == 0 && groups == 0)
|
||||||
|
return "Aucun pair ou syncshell n'avait besoin d'être modifié.";
|
||||||
|
|
||||||
|
if (pairs > 0 && groups > 0)
|
||||||
|
return $"Mise à jour de {pairs} pair(s) et {groups} syncshell(s).";
|
||||||
|
|
||||||
|
if (pairs > 0)
|
||||||
|
return $"Mise à jour de {pairs} pair(s).";
|
||||||
|
|
||||||
|
return $"Mise à jour de {groups} syncshell(s).";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPairOverrideChanged(PairSyncOverrideChanged message)
|
||||||
|
{
|
||||||
|
var overrides = _configService.Current.PairSyncOverrides ??= new(StringComparer.Ordinal);
|
||||||
|
var entry = overrides.TryGetValue(message.Uid, out var existing) ? existing : new SyncOverrideEntry();
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
if (message.DisableSounds.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableSounds.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableSounds;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableSounds != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableSounds = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.DisableAnimations.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableAnimations.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableAnimations;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableAnimations != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableAnimations = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.DisableVfx.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableVfx.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableVfx;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableVfx != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableVfx = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return;
|
||||||
|
|
||||||
|
if (entry.IsEmpty)
|
||||||
|
overrides.Remove(message.Uid);
|
||||||
|
else
|
||||||
|
overrides[message.Uid] = entry;
|
||||||
|
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGroupOverrideChanged(GroupSyncOverrideChanged message)
|
||||||
|
{
|
||||||
|
var overrides = _configService.Current.GroupSyncOverrides ??= new(StringComparer.Ordinal);
|
||||||
|
var entry = overrides.TryGetValue(message.Gid, out var existing) ? existing : new SyncOverrideEntry();
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
if (message.DisableSounds.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableSounds.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableSounds;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableSounds != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableSounds = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.DisableAnimations.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableAnimations.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableAnimations;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableAnimations != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableAnimations = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.DisableVfx.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableVfx.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableVfx;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableVfx != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableVfx = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return;
|
||||||
|
|
||||||
|
if (entry.IsEmpty)
|
||||||
|
overrides.Remove(message.Gid);
|
||||||
|
else
|
||||||
|
overrides[message.Gid] = entry;
|
||||||
|
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SyncOverrideEntry? TryGetPairOverride(string uid)
|
||||||
|
{
|
||||||
|
var overrides = _configService.Current.PairSyncOverrides;
|
||||||
|
return overrides != null && overrides.TryGetValue(uid, out var entry) ? entry : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SyncOverrideEntry? TryGetGroupOverride(string gid)
|
||||||
|
{
|
||||||
|
var overrides = _configService.Current.GroupSyncOverrides;
|
||||||
|
return overrides != null && overrides.TryGetValue(gid, out var entry) ? entry : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ApplyDefaults(ref UserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
if (overrides?.DisableSounds is bool overrideSounds)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableSounds() != overrideSounds)
|
||||||
|
{
|
||||||
|
permissions.SetDisableSounds(overrideSounds);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableSounds() != config.DefaultDisableSounds)
|
||||||
|
{
|
||||||
|
permissions.SetDisableSounds(config.DefaultDisableSounds);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides?.DisableAnimations is bool overrideAnims)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableAnimations() != overrideAnims)
|
||||||
|
{
|
||||||
|
permissions.SetDisableAnimations(overrideAnims);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations)
|
||||||
|
{
|
||||||
|
permissions.SetDisableAnimations(config.DefaultDisableAnimations);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides?.DisableVfx is bool overrideVfx)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableVFX() != overrideVfx)
|
||||||
|
{
|
||||||
|
permissions.SetDisableVFX(overrideVfx);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableVFX() != config.DefaultDisableVfx)
|
||||||
|
{
|
||||||
|
permissions.SetDisableVFX(config.DefaultDisableVfx);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ApplyDefaults(ref GroupUserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
if (overrides?.DisableSounds is bool overrideSounds)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableSounds() != overrideSounds)
|
||||||
|
{
|
||||||
|
permissions.SetDisableSounds(overrideSounds);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableSounds() != config.DefaultDisableSounds)
|
||||||
|
{
|
||||||
|
permissions.SetDisableSounds(config.DefaultDisableSounds);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides?.DisableAnimations is bool overrideAnims)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableAnimations() != overrideAnims)
|
||||||
|
{
|
||||||
|
permissions.SetDisableAnimations(overrideAnims);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations)
|
||||||
|
{
|
||||||
|
permissions.SetDisableAnimations(config.DefaultDisableAnimations);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides?.DisableVfx is bool overrideVfx)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableVFX() != overrideVfx)
|
||||||
|
{
|
||||||
|
permissions.SetDisableVFX(overrideVfx);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableVFX() != config.DefaultDisableVfx)
|
||||||
|
{
|
||||||
|
permissions.SetDisableVFX(config.DefaultDisableVfx);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
225
MareSynchronos/Services/TemporarySyncshellNotificationService.cs
Normal file
225
MareSynchronos/Services/TemporarySyncshellNotificationService.cs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class TemporarySyncshellNotificationService : MediatorSubscriberBase, IHostedService
|
||||||
|
{
|
||||||
|
private static readonly int[] NotificationThresholdMinutes = [30, 15, 5, 1];
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly Lock _stateLock = new();
|
||||||
|
private readonly Dictionary<string, TrackedGroup> _trackedGroups = new(StringComparer.Ordinal);
|
||||||
|
private CancellationTokenSource? _loopCts;
|
||||||
|
private Task? _loopTask;
|
||||||
|
|
||||||
|
public TemporarySyncshellNotificationService(ILogger<TemporarySyncshellNotificationService> logger, MareMediator mediator, PairManager pairManager, ApiController apiController)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_apiController = apiController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_loopCts = new CancellationTokenSource();
|
||||||
|
Mediator.Subscribe<ConnectedMessage>(this, _ => ResetTrackedGroups());
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, _ => ResetTrackedGroups());
|
||||||
|
_loopTask = Task.Run(() => MonitorLoopAsync(_loopCts.Token), _loopCts.Token);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Mediator.UnsubscribeAll(this);
|
||||||
|
if (_loopCts == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_loopCts.Cancel();
|
||||||
|
if (_loopTask != null)
|
||||||
|
{
|
||||||
|
await _loopTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loopTask = null;
|
||||||
|
_loopCts.Dispose();
|
||||||
|
_loopCts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MonitorLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var delay = TimeSpan.FromSeconds(30);
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CheckGroups();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Failed to check temporary syncshell expirations");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckGroups()
|
||||||
|
{
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
var groupsSnapshot = _pairManager.Groups.Values.ToList();
|
||||||
|
var notifications = new List<NotificationPayload>();
|
||||||
|
var expiredGroups = new List<GroupFullInfoDto>();
|
||||||
|
var seenTemporaryGids = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
using (var guard = _stateLock.EnterScope())
|
||||||
|
{
|
||||||
|
foreach (var group in groupsSnapshot)
|
||||||
|
{
|
||||||
|
if (!group.IsTemporary || group.ExpiresAt == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_apiController.UID) || !string.Equals(group.OwnerUID, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gid = group.Group.GID;
|
||||||
|
seenTemporaryGids.Add(gid);
|
||||||
|
var expiresAtUtc = NormalizeToUtc(group.ExpiresAt.Value);
|
||||||
|
var remaining = expiresAtUtc - nowUtc;
|
||||||
|
|
||||||
|
if (!_trackedGroups.TryGetValue(gid, out var state))
|
||||||
|
{
|
||||||
|
state = new TrackedGroup(expiresAtUtc);
|
||||||
|
_trackedGroups[gid] = state;
|
||||||
|
}
|
||||||
|
else if (state.ExpiresAtUtc != expiresAtUtc)
|
||||||
|
{
|
||||||
|
state.UpdateExpiresAt(expiresAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
_trackedGroups.Remove(gid);
|
||||||
|
expiredGroups.Add(group);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.LastRemaining.HasValue)
|
||||||
|
{
|
||||||
|
state.UpdateRemaining(remaining);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousRemaining = state.LastRemaining.Value;
|
||||||
|
|
||||||
|
foreach (var thresholdMinutes in NotificationThresholdMinutes)
|
||||||
|
{
|
||||||
|
var threshold = TimeSpan.FromMinutes(thresholdMinutes);
|
||||||
|
if (previousRemaining > threshold && remaining <= threshold)
|
||||||
|
{
|
||||||
|
notifications.Add(new NotificationPayload(group, thresholdMinutes, expiresAtUtc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.UpdateRemaining(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
var toRemove = _trackedGroups.Keys.Where(k => !seenTemporaryGids.Contains(k)).ToList();
|
||||||
|
foreach (var gid in toRemove)
|
||||||
|
{
|
||||||
|
_trackedGroups.Remove(gid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var expiredGroup in expiredGroups)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Temporary syncshell {gid} expired locally; removing", expiredGroup.Group.GID);
|
||||||
|
_pairManager.RemoveGroup(expiredGroup.Group);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var notification in notifications)
|
||||||
|
{
|
||||||
|
PublishNotification(notification.Group, notification.ThresholdMinutes, notification.ExpiresAtUtc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishNotification(GroupFullInfoDto group, int thresholdMinutes, DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
string displayName = string.IsNullOrWhiteSpace(group.GroupAlias) ? group.Group.GID : group.GroupAlias!;
|
||||||
|
string threshold = thresholdMinutes == 1 ? "1 minute" : $"{thresholdMinutes} minutes";
|
||||||
|
string expiresLocal = expiresAtUtc.ToLocalTime().ToString("t", CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
|
string message = $"La Syncshell temporaire \"{displayName}\" sera supprimee dans {threshold} (a {expiresLocal}).";
|
||||||
|
Mediator.Publish(new NotificationMessage("Syncshell temporaire", message, NotificationType.Warning, TimeSpan.FromSeconds(6)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime NormalizeToUtc(DateTime expiresAt)
|
||||||
|
{
|
||||||
|
return expiresAt.Kind switch
|
||||||
|
{
|
||||||
|
DateTimeKind.Utc => expiresAt,
|
||||||
|
DateTimeKind.Local => expiresAt.ToUniversalTime(),
|
||||||
|
_ => DateTime.SpecifyKind(expiresAt, DateTimeKind.Utc)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetTrackedGroups()
|
||||||
|
{
|
||||||
|
using (var guard = _stateLock.EnterScope())
|
||||||
|
{
|
||||||
|
_trackedGroups.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TrackedGroup
|
||||||
|
{
|
||||||
|
public TrackedGroup(DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
ExpiresAtUtc = expiresAtUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime ExpiresAtUtc { get; private set; }
|
||||||
|
public TimeSpan? LastRemaining { get; private set; }
|
||||||
|
|
||||||
|
public void UpdateExpiresAt(DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
ExpiresAtUtc = expiresAtUtc;
|
||||||
|
LastRemaining = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateRemaining(TimeSpan remaining)
|
||||||
|
{
|
||||||
|
LastRemaining = remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record NotificationPayload(GroupFullInfoDto Group, int ThresholdMinutes, DateTime ExpiresAtUtc);
|
||||||
|
}
|
||||||
131
MareSynchronos/Services/TypingIndicatorStateService.cs
Normal file
131
MareSynchronos/Services/TypingIndicatorStateService.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposable
|
||||||
|
{
|
||||||
|
private sealed record TypingEntry(UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope);
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, TypingEntry> _typingUsers = new(StringComparer.Ordinal);
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly ILogger<TypingIndicatorStateService> _logger;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private DateTime _selfTypingLast = DateTime.MinValue;
|
||||||
|
private DateTime _selfTypingStart = DateTime.MinValue;
|
||||||
|
private bool _selfTypingActive;
|
||||||
|
|
||||||
|
public TypingIndicatorStateService(ILogger<TypingIndicatorStateService> logger, MareMediator mediator, ApiController apiController, MareConfigService configService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_apiController = apiController;
|
||||||
|
_configService = configService;
|
||||||
|
Mediator = mediator;
|
||||||
|
|
||||||
|
mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Mediator.UnsubscribeAll(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator { get; }
|
||||||
|
|
||||||
|
public void SetSelfTypingLocal(bool isTyping)
|
||||||
|
{
|
||||||
|
if (isTyping)
|
||||||
|
{
|
||||||
|
if (!_selfTypingActive)
|
||||||
|
_selfTypingStart = DateTime.UtcNow;
|
||||||
|
_selfTypingLast = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selfTypingStart = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selfTypingActive = isTyping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearAll()
|
||||||
|
{
|
||||||
|
_typingUsers.Clear();
|
||||||
|
_selfTypingActive = false;
|
||||||
|
_selfTypingStart = DateTime.MinValue;
|
||||||
|
_selfTypingLast = DateTime.MinValue;
|
||||||
|
_logger.LogDebug("TypingIndicatorStateService: cleared all typing state");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTypingState(UserTypingStateMessage msg)
|
||||||
|
{
|
||||||
|
if (!_configService.Current.TypingIndicatorEnabled)
|
||||||
|
return;
|
||||||
|
var uid = msg.Typing.User.UID;
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (string.Equals(uid, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_selfTypingActive = msg.Typing.IsTyping;
|
||||||
|
if (_selfTypingActive)
|
||||||
|
{
|
||||||
|
if (_selfTypingStart == DateTime.MinValue)
|
||||||
|
_selfTypingStart = now;
|
||||||
|
_selfTypingLast = now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selfTypingStart = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
_logger.LogInformation("Typing state self -> {state}", _selfTypingActive);
|
||||||
|
}
|
||||||
|
else if (msg.Typing.IsTyping)
|
||||||
|
{
|
||||||
|
_typingUsers.AddOrUpdate(uid,
|
||||||
|
_ => new TypingEntry(msg.Typing.User, now, now, msg.Typing.Scope),
|
||||||
|
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now, msg.Typing.Scope));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_typingUsers.TryRemove(uid, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetSelfTyping(TimeSpan maxAge, out DateTime startTyping, out DateTime lastTyping)
|
||||||
|
{
|
||||||
|
startTyping = _selfTypingStart;
|
||||||
|
lastTyping = _selfTypingLast;
|
||||||
|
if (!_selfTypingActive)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((now - _selfTypingLast) >= maxAge)
|
||||||
|
{
|
||||||
|
_selfTypingActive = false;
|
||||||
|
_selfTypingStart = DateTime.MinValue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope)> GetActiveTypers(TimeSpan maxAge)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var kvp in _typingUsers.ToArray())
|
||||||
|
{
|
||||||
|
if ((now - kvp.Value.LastUpdate) >= maxAge)
|
||||||
|
{
|
||||||
|
_typingUsers.TryRemove(kvp.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _typingUsers.ToDictionary(k => k.Key, v => (v.Value.User, v.Value.FirstSeen, v.Value.LastUpdate, v.Value.Scope), StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.API.Dto.Group;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.Services.Notifications;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
using MareSynchronos.UI.Components.Popup;
|
using MareSynchronos.UI.Components.Popup;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
@@ -19,31 +21,35 @@ public class UiFactory
|
|||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly MareProfileManager _mareProfileManager;
|
private readonly MareProfileManager _mareProfileManager;
|
||||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||||
|
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
|
||||||
|
private readonly NotificationTracker _notificationTracker;
|
||||||
|
|
||||||
public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController,
|
public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController,
|
||||||
UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager,
|
UiSharedService uiSharedService, PairManager pairManager, SyncshellDiscoveryService syncshellDiscoveryService, ServerConfigurationManager serverConfigManager,
|
||||||
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
|
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService, NotificationTracker notificationTracker)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mareMediator = mareMediator;
|
_mareMediator = mareMediator;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_syncshellDiscoveryService = syncshellDiscoveryService;
|
||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
_mareProfileManager = mareProfileManager;
|
_mareProfileManager = mareProfileManager;
|
||||||
_performanceCollectorService = performanceCollectorService;
|
_performanceCollectorService = performanceCollectorService;
|
||||||
|
_notificationTracker = notificationTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
||||||
{
|
{
|
||||||
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _mareMediator,
|
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _mareMediator,
|
||||||
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService);
|
_apiController, _uiSharedService, _pairManager, _syncshellDiscoveryService, dto, _performanceCollectorService, _notificationTracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
||||||
{
|
{
|
||||||
return new StandaloneProfileUi(_loggerFactory.CreateLogger<StandaloneProfileUi>(), _mareMediator,
|
return new StandaloneProfileUi(_loggerFactory.CreateLogger<StandaloneProfileUi>(), _mareMediator,
|
||||||
_uiSharedService, _serverConfigManager, _mareProfileManager, _pairManager, pair, _performanceCollectorService);
|
_uiSharedService, _serverConfigManager, _mareProfileManager, pair, _performanceCollectorService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PermissionWindowUI CreatePermissionPopupUi(Pair pair)
|
public PermissionWindowUI CreatePermissionPopupUi(Pair pair)
|
||||||
|
|||||||
532
MareSynchronos/UI/AutoDetectUi.cs
Normal file
532
MareSynchronos/UI/AutoDetectUi.cs
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
|
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||||
|
|
||||||
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
|
public class AutoDetectUi : WindowMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly DalamudUtilService _dalamud;
|
||||||
|
private readonly AutoDetectRequestService _requestService;
|
||||||
|
private readonly NearbyDiscoveryService _discoveryService;
|
||||||
|
private readonly NearbyPendingService _pendingService;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private List<Services.Mediator.NearbyEntry> _entries;
|
||||||
|
private readonly HashSet<string> _acceptInFlight = new(StringComparer.Ordinal);
|
||||||
|
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
|
||||||
|
private List<SyncshellDiscoveryEntryDto> _syncshellEntries = [];
|
||||||
|
private bool _syncshellInitialized;
|
||||||
|
private readonly HashSet<string> _syncshellJoinInFlight = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private string? _syncshellLastError;
|
||||||
|
|
||||||
|
public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator,
|
||||||
|
MareConfigService configService, DalamudUtilService dalamudUtilService,
|
||||||
|
AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager,
|
||||||
|
NearbyDiscoveryService discoveryService, SyncshellDiscoveryService syncshellDiscoveryService,
|
||||||
|
PerformanceCollectorService performanceCollectorService)
|
||||||
|
: base(logger, mediator, "AutoDetect", performanceCollectorService)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
_dalamud = dalamudUtilService;
|
||||||
|
_requestService = requestService;
|
||||||
|
_pendingService = pendingService;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_discoveryService = discoveryService;
|
||||||
|
_syncshellDiscoveryService = syncshellDiscoveryService;
|
||||||
|
Mediator.Subscribe<Services.Mediator.DiscoveryListUpdated>(this, OnDiscoveryUpdated);
|
||||||
|
Mediator.Subscribe<SyncshellDiscoveryUpdated>(this, OnSyncshellDiscoveryUpdated);
|
||||||
|
_entries = _discoveryService.SnapshotEntries();
|
||||||
|
|
||||||
|
Flags |= ImGuiWindowFlags.NoScrollbar;
|
||||||
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
|
{
|
||||||
|
MinimumSize = new Vector2(350, 220),
|
||||||
|
MaximumSize = new Vector2(600, 600),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool DrawConditions()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
using var idScope = ImRaii.PushId("autodetect-ui");
|
||||||
|
|
||||||
|
var incomingInvites = _pendingService.Pending.ToList();
|
||||||
|
var outgoingInvites = _requestService.GetPendingRequestsSnapshot();
|
||||||
|
|
||||||
|
Vector4 accent = UiSharedService.AccentColor;
|
||||||
|
if (accent.W <= 0f) accent = ImGuiColors.ParsedPurple;
|
||||||
|
Vector4 inactiveTab = new(accent.X * 0.45f, accent.Y * 0.45f, accent.Z * 0.45f, Math.Clamp(accent.W + 0.15f, 0f, 1f));
|
||||||
|
Vector4 hoverTab = UiSharedService.AccentHoverColor;
|
||||||
|
|
||||||
|
using var tabs = ImRaii.TabBar("AutoDetectTabs");
|
||||||
|
if (!tabs.Success) return;
|
||||||
|
|
||||||
|
var incomingCount = incomingInvites.Count;
|
||||||
|
DrawStyledTab($"Invitations ({incomingCount})", accent, inactiveTab, hoverTab, () =>
|
||||||
|
{
|
||||||
|
DrawInvitationsTab(incomingInvites, outgoingInvites);
|
||||||
|
});
|
||||||
|
|
||||||
|
DrawStyledTab("Proximité", accent, inactiveTab, hoverTab, DrawNearbyTab);
|
||||||
|
DrawStyledTab("Syncshell", accent, inactiveTab, hoverTab, DrawSyncshellTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawInline()
|
||||||
|
{
|
||||||
|
DrawInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawStyledTab(string label, Vector4 accent, Vector4 inactive, Vector4 hover, Action draw, bool disabled = false)
|
||||||
|
{
|
||||||
|
var tabColor = disabled ? ImGuiColors.DalamudGrey3 : inactive;
|
||||||
|
var tabHover = disabled ? ImGuiColors.DalamudGrey3 : hover;
|
||||||
|
var tabActive = disabled ? ImGuiColors.DalamudGrey2 : accent;
|
||||||
|
using var baseColor = ImRaii.PushColor(ImGuiCol.Tab, tabColor);
|
||||||
|
using var hoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, tabHover);
|
||||||
|
using var activeColor = ImRaii.PushColor(ImGuiCol.TabActive, tabActive);
|
||||||
|
using var activeText = ImRaii.PushColor(ImGuiCol.Text, disabled ? ImGuiColors.DalamudGrey2 : Vector4.One, false);
|
||||||
|
using var tab = ImRaii.TabItem(label);
|
||||||
|
if (tab.Success)
|
||||||
|
{
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawInvitationsTab(List<KeyValuePair<string, string>> incomingInvites, IReadOnlyCollection<AutoDetectRequestService.PendingRequestInfo> outgoingInvites)
|
||||||
|
{
|
||||||
|
if (incomingInvites.Count == 0 && outgoingInvites.Count == 0)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Aucune invitation en attente. Cette page regroupera les demandes reçues et celles que vous avez envoyées.", ImGuiColors.DalamudGrey3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingInvites.Count == 0)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Vous n'avez aucune invitation de pair en attente pour le moment.", ImGuiColors.DalamudGrey3);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
float leftWidth = Math.Max(220f * ImGuiHelpers.GlobalScale, ImGui.CalcTextSize("Invitations reçues (00)").X + ImGui.GetStyle().FramePadding.X * 4f);
|
||||||
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
|
|
||||||
|
ImGui.BeginChild("incoming-requests", new Vector2(leftWidth, avail.Y), true);
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations reçues ({incomingInvites.Count})");
|
||||||
|
ImGui.Separator();
|
||||||
|
if (incomingInvites.Count == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucune invitation reçue.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var (uid, name) in incomingInvites.OrderBy(k => k.Value, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId(uid);
|
||||||
|
bool processing = _acceptInFlight.Contains(uid);
|
||||||
|
ImGui.TextUnformatted(name);
|
||||||
|
ImGui.TextDisabled(uid);
|
||||||
|
if (processing)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Traitement en cours...");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Accepter"))
|
||||||
|
{
|
||||||
|
TriggerAccept(uid);
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Refuser"))
|
||||||
|
{
|
||||||
|
_pendingService.Remove(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.EndChild();
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
ImGui.BeginChild("outgoing-requests", new Vector2(0, avail.Y), true);
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations envoyées ({outgoingInvites.Count})");
|
||||||
|
ImGui.Separator();
|
||||||
|
if (outgoingInvites.Count == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucune invitation envoyée en attente.");
|
||||||
|
ImGui.EndChild();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var info in outgoingInvites.OrderByDescending(i => i.SentAt))
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId(info.Key);
|
||||||
|
ImGui.TextUnformatted(info.TargetDisplayName);
|
||||||
|
if (!string.IsNullOrEmpty(info.Uid))
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled(info.Uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TextDisabled($"Envoyée il y a {FormatDuration(DateTime.UtcNow - info.SentAt)}");
|
||||||
|
if (ImGui.Button("Retirer"))
|
||||||
|
{
|
||||||
|
_requestService.RemovePendingRequestByKey(info.Key);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Retire uniquement cette entrée locale de suivi.");
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNearbyTab()
|
||||||
|
{
|
||||||
|
if (!_configService.Current.EnableAutoDetectDiscovery)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("AutoDetect est désactivé. Activez-le dans les paramètres pour détecter les utilisateurs Umbra à proximité.", ImGuiColors.DalamudYellow);
|
||||||
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxDist = Math.Clamp(_configService.Current.AutoDetectMaxDistanceMeters, 5, 100);
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted("Max distance (m)");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetNextItemWidth(120 * ImGuiHelpers.GlobalScale);
|
||||||
|
if (ImGui.SliderInt("##autodetect-dist", ref maxDist, 5, 100))
|
||||||
|
{
|
||||||
|
_configService.Current.AutoDetectMaxDistanceMeters = maxDist;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
|
||||||
|
var sourceEntries = _entries.Count > 0 ? _entries : _discoveryService.SnapshotEntries();
|
||||||
|
var orderedEntries = sourceEntries
|
||||||
|
.Where(e => e.IsMatch)
|
||||||
|
.OrderBy(e => float.IsNaN(e.Distance) ? float.MaxValue : e.Distance)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (orderedEntries.Count == 0)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Aucune présence UmbraSync détectée à proximité pour le moment.", ImGuiColors.DalamudGrey3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ImGui.BeginTable("autodetect-nearby", 5, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableSetupColumn("Nom");
|
||||||
|
ImGui.TableSetupColumn("Monde");
|
||||||
|
ImGui.TableSetupColumn("Distance");
|
||||||
|
ImGui.TableSetupColumn("Statut");
|
||||||
|
ImGui.TableSetupColumn("Action");
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
for (int i = 0; i < orderedEntries.Count; i++)
|
||||||
|
{
|
||||||
|
var entry = orderedEntries[i];
|
||||||
|
bool alreadyPaired = IsAlreadyPairedByUidOrAlias(entry);
|
||||||
|
bool overDistance = !float.IsNaN(entry.Distance) && entry.Distance > maxDist;
|
||||||
|
bool canRequest = entry.AcceptPairRequests && !string.IsNullOrEmpty(entry.Token) && !alreadyPaired;
|
||||||
|
|
||||||
|
string displayName = entry.DisplayName ?? entry.Name;
|
||||||
|
string worldName = entry.WorldId == 0
|
||||||
|
? "-"
|
||||||
|
: (_dalamud.WorldData.Value.TryGetValue(entry.WorldId, out var mappedWorld) ? mappedWorld : entry.WorldId.ToString(CultureInfo.InvariantCulture));
|
||||||
|
string distanceText = float.IsNaN(entry.Distance) ? "-" : $"{entry.Distance:0.0} m";
|
||||||
|
|
||||||
|
string status = alreadyPaired
|
||||||
|
? "Déjà appairé"
|
||||||
|
: overDistance
|
||||||
|
? $"Hors portée (> {maxDist} m)"
|
||||||
|
: !entry.AcceptPairRequests
|
||||||
|
? "Invitations refusées"
|
||||||
|
: string.IsNullOrEmpty(entry.Token)
|
||||||
|
? "Indisponible"
|
||||||
|
: "Disponible";
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(displayName);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(worldName);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(distanceText);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(status);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.PushId(i))
|
||||||
|
{
|
||||||
|
if (canRequest && !overDistance)
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Envoyer invitation"))
|
||||||
|
{
|
||||||
|
_ = _requestService.SendRequestAsync(entry.Token!, entry.Uid, entry.DisplayName);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Envoie une demande d'appairage via AutoDetect.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string reason = alreadyPaired
|
||||||
|
? "Vous êtes déjà appairé avec ce joueur."
|
||||||
|
: overDistance
|
||||||
|
? $"Ce joueur est au-delà de la distance maximale configurée ({maxDist} m)."
|
||||||
|
: !entry.AcceptPairRequests
|
||||||
|
? "Ce joueur a désactivé la réception automatique des invitations."
|
||||||
|
: string.IsNullOrEmpty(entry.Token)
|
||||||
|
? "Impossible d'obtenir un jeton d'invitation pour ce joueur."
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
ImGui.TextDisabled(status);
|
||||||
|
if (!string.IsNullOrEmpty(reason))
|
||||||
|
{
|
||||||
|
UiSharedService.AttachToolTip(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task JoinSyncshellAsync(SyncshellDiscoveryEntryDto entry)
|
||||||
|
{
|
||||||
|
if (!_syncshellJoinInFlight.Add(entry.GID))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var joined = await _syncshellDiscoveryService.JoinAsync(entry.GID, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (joined)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new NotificationMessage("AutoDetect Syncshell", $"Rejoint {entry.Alias ?? entry.GID}.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
|
await _syncshellDiscoveryService.RefreshAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_syncshellLastError = $"Impossible de rejoindre {entry.Alias ?? entry.GID}.";
|
||||||
|
Mediator.Publish(new NotificationMessage("AutoDetect Syncshell", _syncshellLastError, NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_syncshellLastError = $"Erreur lors de l'adhésion : {ex.Message}";
|
||||||
|
Mediator.Publish(new NotificationMessage("AutoDetect Syncshell", _syncshellLastError, NotificationType.Error, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_syncshellJoinInFlight.Remove(entry.GID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSyncshellTab()
|
||||||
|
{
|
||||||
|
if (!_syncshellInitialized)
|
||||||
|
{
|
||||||
|
_syncshellInitialized = true;
|
||||||
|
_ = _syncshellDiscoveryService.RefreshAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isRefreshing = _syncshellDiscoveryService.IsRefreshing;
|
||||||
|
var serviceError = _syncshellDiscoveryService.LastError;
|
||||||
|
|
||||||
|
if (ImGui.Button("Actualiser la liste"))
|
||||||
|
{
|
||||||
|
_ = _syncshellDiscoveryService.RefreshAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Met à jour la liste des Syncshells ayant activé l'AutoDetect.");
|
||||||
|
|
||||||
|
if (isRefreshing)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextDisabled("Actualisation...");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
UiSharedService.TextWrapped("Les Syncshells affichées ont temporairement désactivé leur mot de passe pour permettre un accès direct via AutoDetect. Rejoignez-les uniquement si vous faites confiance aux administrateurs.");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(serviceError))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(serviceError, ImGuiColors.DalamudRed);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(_syncshellLastError))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(_syncshellLastError!, ImGuiColors.DalamudOrange);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = _syncshellEntries.Count > 0 ? _syncshellEntries : _syncshellDiscoveryService.Entries.ToList();
|
||||||
|
if (entries.Count == 0)
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
UiSharedService.ColorTextWrapped("Aucune Syncshell n'est actuellement visible dans AutoDetect.", ImGuiColors.DalamudGrey3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ImGui.BeginTable("autodetect-syncshells", 5, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableSetupColumn("Nom");
|
||||||
|
ImGui.TableSetupColumn("Propriétaire");
|
||||||
|
ImGui.TableSetupColumn("Membres");
|
||||||
|
ImGui.TableSetupColumn("Invitations");
|
||||||
|
ImGui.TableSetupColumn("Action");
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
foreach (var entry in entries.OrderBy(e => e.Alias ?? e.GID, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
bool alreadyMember = _pairManager.Groups.Keys.Any(g => string.Equals(g.GID, entry.GID, StringComparison.OrdinalIgnoreCase));
|
||||||
|
bool joining = _syncshellJoinInFlight.Contains(entry.GID);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.Alias) ? entry.GID : $"{entry.Alias} ({entry.GID})");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.OwnerAlias) ? entry.OwnerUID : $"{entry.OwnerAlias} ({entry.OwnerUID})");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.MemberCount.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
string inviteMode = entry.AutoAcceptPairs ? "Auto" : "Manuel";
|
||||||
|
ImGui.TextUnformatted(inviteMode);
|
||||||
|
if (!entry.AutoAcceptPairs)
|
||||||
|
{
|
||||||
|
UiSharedService.AttachToolTip("L'administrateur doit approuver manuellement les nouveaux membres.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.Disabled(alreadyMember || joining))
|
||||||
|
{
|
||||||
|
if (alreadyMember)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Déjà membre");
|
||||||
|
}
|
||||||
|
else if (joining)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Connexion...");
|
||||||
|
}
|
||||||
|
else if (ImGui.Button("Rejoindre"))
|
||||||
|
{
|
||||||
|
_syncshellLastError = null;
|
||||||
|
_ = JoinSyncshellAsync(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDiscoveryUpdated(Services.Mediator.DiscoveryListUpdated msg)
|
||||||
|
{
|
||||||
|
_entries = msg.Entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSyncshellDiscoveryUpdated(SyncshellDiscoveryUpdated msg)
|
||||||
|
{
|
||||||
|
_syncshellEntries = msg.Entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1) Match by UID when available (authoritative)
|
||||||
|
if (!string.IsNullOrEmpty(e.Uid))
|
||||||
|
{
|
||||||
|
foreach (var p in _pairManager.DirectPairs)
|
||||||
|
{
|
||||||
|
if (string.Equals(p.UserData.UID, e.Uid, StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var key = NormalizeKey(e.DisplayName ?? e.Name);
|
||||||
|
if (string.IsNullOrEmpty(key)) return false;
|
||||||
|
foreach (var p in _pairManager.DirectPairs)
|
||||||
|
{
|
||||||
|
if (NormalizeKey(p.UserData.AliasOrUID) == key) return true;
|
||||||
|
if (!string.IsNullOrEmpty(p.UserData.Alias) && NormalizeKey(p.UserData.Alias!) == key) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeKey(string? input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
|
||||||
|
var formD = input.Normalize(NormalizationForm.FormD);
|
||||||
|
var sb = new StringBuilder(formD.Length);
|
||||||
|
foreach (var ch in formD)
|
||||||
|
{
|
||||||
|
var cat = CharUnicodeInfo.GetUnicodeCategory(ch);
|
||||||
|
if (cat != UnicodeCategory.NonSpacingMark)
|
||||||
|
sb.Append(char.ToLowerInvariant(ch));
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TriggerAccept(string uid)
|
||||||
|
{
|
||||||
|
if (!_acceptInFlight.Add(uid)) return;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool ok = await _pendingService.AcceptAsync(uid).ConfigureAwait(false);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new NotificationMessage("AutoDetect", $"Impossible d'accepter l'invitation {uid}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_acceptInFlight.Remove(uid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatDuration(TimeSpan span)
|
||||||
|
{
|
||||||
|
if (span.TotalMinutes >= 1)
|
||||||
|
{
|
||||||
|
var minutes = Math.Max(1, (int)Math.Round(span.TotalMinutes));
|
||||||
|
return minutes == 1 ? "1 minute" : $"{minutes} minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds = Math.Max(1, (int)Math.Round(span.TotalSeconds));
|
||||||
|
return seconds == 1 ? "1 seconde" : $"{seconds} secondes";
|
||||||
|
}
|
||||||
|
}
|
||||||
245
MareSynchronos/UI/ChangelogUi.cs
Normal file
245
MareSynchronos/UI/ChangelogUi.cs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
|
public sealed class ChangelogUi : WindowMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private const int AlwaysExpandedEntryCount = 2;
|
||||||
|
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly UiSharedService _uiShared;
|
||||||
|
private readonly Version _currentVersion;
|
||||||
|
private readonly string _currentVersionLabel;
|
||||||
|
private readonly IReadOnlyList<ChangelogEntry> _entries;
|
||||||
|
|
||||||
|
private bool _showAllEntries;
|
||||||
|
private bool _hasAcknowledgedVersion;
|
||||||
|
|
||||||
|
public ChangelogUi(ILogger<ChangelogUi> logger, UiSharedService uiShared, MareConfigService configService,
|
||||||
|
MareMediator mediator, PerformanceCollectorService performanceCollectorService)
|
||||||
|
: base(logger, mediator, "Umbra Sync - Notes de version", performanceCollectorService)
|
||||||
|
{
|
||||||
|
_uiShared = uiShared;
|
||||||
|
_configService = configService;
|
||||||
|
_currentVersion = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0);
|
||||||
|
_currentVersionLabel = _currentVersion.ToString();
|
||||||
|
_entries = BuildEntries();
|
||||||
|
_hasAcknowledgedVersion = string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
RespectCloseHotkey = true;
|
||||||
|
SizeConstraints = new()
|
||||||
|
{
|
||||||
|
MinimumSize = new(520, 360),
|
||||||
|
MaximumSize = new(900, 1200)
|
||||||
|
};
|
||||||
|
Flags |= ImGuiWindowFlags.NoResize;
|
||||||
|
ShowCloseButton = true;
|
||||||
|
|
||||||
|
if (!string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
IsOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnClose()
|
||||||
|
{
|
||||||
|
MarkCurrentVersionAsReadIfNeeded();
|
||||||
|
base.OnClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
_ = _uiShared.DrawOtherPluginState();
|
||||||
|
|
||||||
|
DrawHeader();
|
||||||
|
DrawEntries();
|
||||||
|
DrawFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHeader()
|
||||||
|
{
|
||||||
|
using (_uiShared.UidFont.Push())
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Notes de version");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudGrey, $"Version chargée : {_currentVersionLabel}");
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawEntries()
|
||||||
|
{
|
||||||
|
bool expandedOldVersions = false;
|
||||||
|
for (int index = 0; index < _entries.Count; index++)
|
||||||
|
{
|
||||||
|
var entry = _entries[index];
|
||||||
|
if (!_showAllEntries && index >= AlwaysExpandedEntryCount)
|
||||||
|
{
|
||||||
|
if (!expandedOldVersions)
|
||||||
|
{
|
||||||
|
expandedOldVersions = ImGui.CollapsingHeader("Historique complet");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expandedOldVersions)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawEntry(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawEntry(ChangelogEntry entry)
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId(entry.VersionLabel))
|
||||||
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
UiSharedService.ColorText(entry.VersionLabel, entry.Version == _currentVersion
|
||||||
|
? ImGuiColors.HealerGreen
|
||||||
|
: ImGuiColors.DalamudWhite);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
foreach (var line in entry.Lines)
|
||||||
|
{
|
||||||
|
DrawLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawLine(ChangelogLine line)
|
||||||
|
{
|
||||||
|
using var indent = line.IndentLevel > 0 ? ImRaii.PushIndent(line.IndentLevel) : null;
|
||||||
|
if (line.Color != null)
|
||||||
|
{
|
||||||
|
ImGui.TextColored(line.Color.Value, $"- {line.Text}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted($"- {line.Text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawFooter()
|
||||||
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
if (!_showAllEntries && _entries.Count > AlwaysExpandedEntryCount)
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Tout afficher"))
|
||||||
|
{
|
||||||
|
_showAllEntries = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Button("Marquer comme lu"))
|
||||||
|
{
|
||||||
|
MarkCurrentVersionAsReadIfNeeded();
|
||||||
|
IsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkCurrentVersionAsReadIfNeeded()
|
||||||
|
{
|
||||||
|
if (_hasAcknowledgedVersion)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_configService.Current.LastChangelogVersionSeen = _currentVersionLabel;
|
||||||
|
_configService.Save();
|
||||||
|
_hasAcknowledgedVersion = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<ChangelogEntry> BuildEntries()
|
||||||
|
{
|
||||||
|
return new List<ChangelogEntry>
|
||||||
|
{
|
||||||
|
new(new Version(0, 1, 9, 5), "0.1.9.5", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Fix l'affichage de la bulle dans la liste du groupe."),
|
||||||
|
new("Amélioration de l'ajout des utilisateurs via le bouton +."),
|
||||||
|
new("Possibilité de mettre en pause individuellement des utilisateurs d'une syncshell."),
|
||||||
|
new("Amélioration de la stabilité du plugin en cas de petite connexion / petite configuration."),
|
||||||
|
new("Divers fix de l'interface."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 4), "0.1.9.4", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Réécriture complète de la bulle de frappe avec la possibilité de choisir la taille de la bulle."),
|
||||||
|
new("Désactivation de l'AutoDetect en zone instanciée."),
|
||||||
|
new("Réécriture interface AutoDetect pour acceuillir les invitations en attente et préparer les synchsells publiques."),
|
||||||
|
new("Amélioration de la compréhension des activations / désactivations des préférences de synchronisation par défaut."),
|
||||||
|
new("Mise en avant du Self Analyse avec une alerte lorsqu'un seuil de donnée a été atteint."),
|
||||||
|
new("Ajout de l'alerte de la non-compatibilité du plugin Chat2."),
|
||||||
|
new("Divers fix de l'interface."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 3), "0.1.9.3", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Correctif de l'affichage de la bulle de frappe quand l'interface est à + de 100%."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 2), "0.1.9.2", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Correctif de l'affichage de la bulle de frappe."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 1), "0.1.9.1", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Début correctif pour la bulle de frappe."),
|
||||||
|
new("Les bascules de synchronisation n'affichent plus qu'une seule notification résumée."),
|
||||||
|
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 0), "0.1.9.0", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Il est désormais possible de configurer par défaut nos choix de synchronisation (VFX, Music, Animation)."),
|
||||||
|
new("La catégorie 'En attente' ne s'affiche uniquement que si une invitation est en attente"),
|
||||||
|
new("(EN PRÉ VERSION) Il est désormais possible de voir quand une personne appairée est en train d'écrire avec une bulle qui s'affiche."),
|
||||||
|
new("(EN PRÉ VERSION) La bulle de frappe s'affiche également sur votre propre plaque de nom lorsque vous écrivez."),
|
||||||
|
new("Les bascules de synchronisation n'affichent plus qu'une seule notification résumée."),
|
||||||
|
new("Correctif : Désormais, les invitation entrantes ne s'affichent qu'une seule fois au lieu de deux."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 8, 2), "0.1.8.2", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Détection Nearby : la liste rapide ne montre plus que les joueurs réellement invitables."),
|
||||||
|
new("Sont filtrés automatiquement les personnes refusées ou déjà appairées."),
|
||||||
|
new("Invitations Nearby : anti-spam de 5 minutes par personne, blocage 15 minutes après trois refus."),
|
||||||
|
new("Affichage : Correction de l'affichage des notes par défaut plutôt que de l'ID si disponible."),
|
||||||
|
new("Les notifications de blocage sont envoyées directement dans le tchat."),
|
||||||
|
new("Overlay DTR : affiche le nombre d'invitations Nearby disponibles dans le titre et l'infobulle."),
|
||||||
|
new("Poses Nearby : le filtre re-fonctionne avec vos notes locales pour retrouver les entrées correspondantes."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 8, 1), "0.1.8.1", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Correctif 'Vu sous' : l'infobulle affiche désormais le dernier personnage observé."),
|
||||||
|
new("Invitations AutoDetect : triées en tête de liste pour mieux les repérer."),
|
||||||
|
new("Invitations AutoDetect : conservées entre les redémarrages du plugin ou du jeu."),
|
||||||
|
new("Barre de statut serveur : couleur violette adoptée par défaut."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 8, 0), "0.1.8.0", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("AutoDetect : détection automatique des joueurs Umbra autour de vous et propositions d'appairage."),
|
||||||
|
new("AutoDetect : désactivé par défaut pour préserver la confidentialité.", 1, ImGuiColors.DalamudGrey),
|
||||||
|
new("AutoDetect : activez-le dans 'Transfers' avec les options Nearby detection et Allow pair requests.", 1, ImGuiColors.DalamudGrey),
|
||||||
|
new("Syncshell temporaire : durée configurable de 1 h à 7 jours, expiration automatique."),
|
||||||
|
new("Syncshell permanente : possibilité de nommer et d'organiser vos groupes sur la durée."),
|
||||||
|
new("Interface : palette UmbraSync harmonisée et menus allégés pour l'usage RP."),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct ChangelogEntry(Version Version, string VersionLabel, IReadOnlyList<ChangelogLine> Lines);
|
||||||
|
|
||||||
|
private readonly record struct ChangelogLine(string Text, int IndentLevel = 0, System.Numerics.Vector4? Color = null);
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ using System.Text;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
|
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using MareSynchronos.Services.CharaData.Models;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private string _joinLobbyId = string.Empty;
|
private string _joinLobbyId = string.Empty;
|
||||||
private void DrawGposeTogether()
|
private void DrawGposeTogether()
|
||||||
@@ -15,14 +15,14 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (!_charaDataManager.BrioAvailable)
|
if (!_charaDataManager.BrioAvailable)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", ImGuiColors.DalamudRed);
|
UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_uiSharedService.ApiController.IsConnected)
|
if (!_uiSharedService.ApiController.IsConnected)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", ImGuiColors.DalamudRed);
|
UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (!_uiSharedService.IsInGpose)
|
if (!_uiSharedService.IsInGpose)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", ImGuiColors.DalamudYellow, 300);
|
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", UiSharedService.AccentColor, 300);
|
||||||
}
|
}
|
||||||
UiSharedService.DistanceSeparator();
|
UiSharedService.DistanceSeparator();
|
||||||
ImGui.TextUnformatted("Users In Lobby");
|
ImGui.TextUnformatted("Users In Lobby");
|
||||||
@@ -104,7 +104,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
|
|
||||||
if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
|
if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
|
||||||
{
|
{
|
||||||
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -165,7 +165,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator);
|
UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator);
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
|
||||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null)
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null)
|
||||||
{
|
{
|
||||||
_dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map);
|
_dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map);
|
||||||
@@ -175,12 +175,12 @@ internal sealed partial class CharaDataHubUi
|
|||||||
+ "Note: For GPose synchronization to work properly, you must be on the same map.");
|
+ "Note: For GPose synchronization to work properly, you must be on the same map.");
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator
|
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator
|
||||||
+ "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users.");
|
+ "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users.");
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator +
|
UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator +
|
||||||
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
|
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
|
||||||
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");
|
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");
|
||||||
@@ -217,7 +217,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (_uiSharedService.IsInGpose && user.Address == nint.Zero)
|
if (_uiSharedService.IsInGpose && user.Address == nint.Zero)
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudRed);
|
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied.");
|
UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private void DrawEditCharaData(CharaDataFullExtendedDto? dataDto)
|
private void DrawEditCharaData(CharaDataFullExtendedDto? dataDto)
|
||||||
{
|
{
|
||||||
@@ -18,7 +18,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (dataDto == null)
|
if (dataDto == null)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", UiSharedService.AccentColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
|
|
||||||
if (updateDto == null)
|
if (updateDto == null)
|
||||||
{
|
{
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", UiSharedService.AccentColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (canUpdate)
|
if (canUpdate)
|
||||||
{
|
{
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", UiSharedService.AccentColor);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
|
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
|
||||||
{
|
{
|
||||||
@@ -61,7 +61,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
}
|
}
|
||||||
if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)
|
if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
{
|
{
|
||||||
if (_charaDataManager.UploadProgress != null)
|
if (_charaDataManager.UploadProgress != null)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload"))
|
if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload"))
|
||||||
{
|
{
|
||||||
@@ -216,7 +216,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", UiSharedService.AccentColor);
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
ImGui.SameLine(pos);
|
ImGui.SameLine(pos);
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data"))
|
||||||
@@ -230,7 +230,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGuiHelpers.ScaledDummy(20, 1);
|
ImGuiHelpers.ScaledDummy(20, 1);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.TextUnformatted("Contains Manipulation Data");
|
ImGui.TextUnformatted("Contains Manipulation Data");
|
||||||
@@ -385,7 +385,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, poseCount == maxPoses))
|
using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, poseCount == maxPoses))
|
||||||
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
|
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
@@ -395,13 +395,13 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable)
|
if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
else if (!_charaDataManager.BrioAvailable)
|
else if (!_charaDataManager.BrioAvailable)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", ImGuiColors.DalamudRed);
|
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (pose.Id == null)
|
if (pose.Id == null)
|
||||||
{
|
{
|
||||||
ImGui.SameLine(50);
|
ImGui.SameLine(50);
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Plus, ImGuiColors.DalamudYellow);
|
_uiSharedService.IconText(FontAwesomeIcon.Plus, UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data.");
|
UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,14 +422,14 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (poseHasChanges)
|
if (poseHasChanges)
|
||||||
{
|
{
|
||||||
ImGui.SameLine(50);
|
ImGui.SameLine(50);
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
|
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet.");
|
UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine(75);
|
ImGui.SameLine(75);
|
||||||
if (pose.Description == null && pose.WorldData == null && pose.PoseData == null)
|
if (pose.Description == null && pose.WorldData == null && pose.PoseData == null)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorText("Pose scheduled for deletion", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorText("Pose scheduled for deletion", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -544,7 +544,8 @@ internal sealed partial class CharaDataHubUi
|
|||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Online Character Data from Server"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Online Character Data from Server"))
|
||||||
{
|
{
|
||||||
_ = _charaDataManager.GetAllData(_disposalCts.Token);
|
var cts = EnsureFreshCts(ref _disposalCts);
|
||||||
|
_ = _charaDataManager.GetAllData(cts.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
|
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
|
||||||
@@ -585,7 +586,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
var idText = entry.FullId;
|
var idText = entry.FullId;
|
||||||
if (uDto?.HasChanges ?? false)
|
if (uDto?.HasChanges ?? false)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorText(idText, ImGuiColors.DalamudYellow);
|
UiSharedService.ColorText(idText, UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip("This entry has unsaved changes");
|
UiSharedService.AttachToolTip("This entry has unsaved changes");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -640,7 +641,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
FontAwesomeIcon eIcon = FontAwesomeIcon.None;
|
FontAwesomeIcon eIcon = FontAwesomeIcon.None;
|
||||||
if (!Equals(DateTime.MaxValue, entry.ExpiryDate))
|
if (!Equals(DateTime.MaxValue, entry.ExpiryDate))
|
||||||
eIcon = FontAwesomeIcon.Clock;
|
eIcon = FontAwesomeIcon.Clock;
|
||||||
_uiSharedService.IconText(eIcon, ImGuiColors.DalamudYellow);
|
_uiSharedService.IconText(eIcon, UiSharedService.AccentColor);
|
||||||
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
|
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
|
||||||
if (eIcon != FontAwesomeIcon.None)
|
if (eIcon != FontAwesomeIcon.None)
|
||||||
{
|
{
|
||||||
@@ -654,7 +655,8 @@ internal sealed partial class CharaDataHubUi
|
|||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "New Character Data Entry"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "New Character Data Entry"))
|
||||||
{
|
{
|
||||||
_charaDataManager.CreateCharaDataEntry(_closalCts.Token);
|
var cts = EnsureFreshCts(ref _closalCts);
|
||||||
|
_charaDataManager.CreateCharaDataEntry(cts.Token);
|
||||||
_selectNewEntry = true;
|
_selectNewEntry = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -675,17 +677,17 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)
|
if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)
|
||||||
{
|
{
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted)
|
if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
|
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
|
||||||
{
|
{
|
||||||
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
|
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : UiSharedService.AccentColor;
|
||||||
UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color);
|
UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,4 +850,4 @@ internal sealed partial class CharaDataHubUi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private void DrawNearbyPoses()
|
private void DrawNearbyPoses()
|
||||||
{
|
{
|
||||||
@@ -57,6 +57,14 @@ internal partial class CharaDataHubUi
|
|||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
_uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world.");
|
_uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world.");
|
||||||
|
int maxWisps = _configService.Current.NearbyMaxWisps;
|
||||||
|
ImGui.SetNextItemWidth(140);
|
||||||
|
if (ImGui.SliderInt("Maximum wisps", ref maxWisps, 0, 200))
|
||||||
|
{
|
||||||
|
_configService.Current.NearbyMaxWisps = maxWisps;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
_uiSharedService.DrawHelpText("Limit how many wisps are active at once. Set to 0 to disable wisps even when enabled above.");
|
||||||
int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
|
int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
|
||||||
ImGui.SetNextItemWidth(100);
|
ImGui.SetNextItemWidth(100);
|
||||||
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))
|
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))
|
||||||
@@ -78,7 +86,7 @@ internal partial class CharaDataHubUi
|
|||||||
if (!_uiSharedService.IsInGpose)
|
if (!_uiSharedService.IsInGpose)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +101,7 @@ internal partial class CharaDataHubUi
|
|||||||
using var indent = ImRaii.PushIndent(5f);
|
using var indent = ImRaii.PushIndent(5f);
|
||||||
if (_charaDataNearbyManager.NearbyData.Count == 0)
|
if (_charaDataNearbyManager.NearbyData.Count == 0)
|
||||||
{
|
{
|
||||||
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool wasAnythingHovered = false;
|
bool wasAnythingHovered = false;
|
||||||
@@ -196,7 +204,8 @@ internal partial class CharaDataHubUi
|
|||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Update Data Shared With You"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Update Data Shared With You"))
|
||||||
{
|
{
|
||||||
_ = _charaDataManager.GetAllSharedData(_disposalCts.Token).ContinueWith(u => UpdateFilteredItems());
|
var cts = EnsureFreshCts(ref _disposalCts);
|
||||||
|
_ = _charaDataManager.GetAllSharedData(cts.Token).ContinueWith(u => UpdateFilteredItems());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted)
|
if (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.ImGuiFileDialog;
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
@@ -15,10 +17,13 @@ using MareSynchronos.Services.Mediator;
|
|||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
public sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private const int maxPoses = 10;
|
private const int maxPoses = 10;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
@@ -30,9 +35,10 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
private readonly CharaDataGposeTogetherManager _charaDataGposeTogetherManager;
|
private readonly CharaDataGposeTogetherManager _charaDataGposeTogetherManager;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private CancellationTokenSource _closalCts = new();
|
private readonly McdfShareManager _mcdfShareManager;
|
||||||
|
private CancellationTokenSource? _closalCts = new();
|
||||||
private bool _disableUI = false;
|
private bool _disableUI = false;
|
||||||
private CancellationTokenSource _disposalCts = new();
|
private CancellationTokenSource? _disposalCts = new();
|
||||||
private string _exportDescription = string.Empty;
|
private string _exportDescription = string.Empty;
|
||||||
private string _filterCodeNote = string.Empty;
|
private string _filterCodeNote = string.Empty;
|
||||||
private string _filterDescription = string.Empty;
|
private string _filterDescription = string.Empty;
|
||||||
@@ -62,6 +68,15 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string SanitizeFileName(string? candidate, string fallback)
|
||||||
|
{
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate)) return fallback;
|
||||||
|
|
||||||
|
var sanitized = new string(candidate.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()).Trim('_');
|
||||||
|
return string.IsNullOrWhiteSpace(sanitized) ? fallback : sanitized;
|
||||||
|
}
|
||||||
private string _selectedSpecificUserIndividual = string.Empty;
|
private string _selectedSpecificUserIndividual = string.Empty;
|
||||||
private string _selectedSpecificGroupIndividual = string.Empty;
|
private string _selectedSpecificGroupIndividual = string.Empty;
|
||||||
private string _sharedWithYouDescriptionFilter = string.Empty;
|
private string _sharedWithYouDescriptionFilter = string.Empty;
|
||||||
@@ -73,12 +88,21 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
private string? _openComboHybridId = null;
|
private string? _openComboHybridId = null;
|
||||||
private (string Id, string? Alias, string AliasOrId, string? Note)[]? _openComboHybridEntries = null;
|
private (string Id, string? Alias, string AliasOrId, string? Note)[]? _openComboHybridEntries = null;
|
||||||
private bool _comboHybridUsedLastFrame = false;
|
private bool _comboHybridUsedLastFrame = false;
|
||||||
|
private bool _mcdfShareInitialized;
|
||||||
|
private string _mcdfShareDescription = string.Empty;
|
||||||
|
private readonly List<string> _mcdfShareAllowedIndividuals = new();
|
||||||
|
private readonly List<string> _mcdfShareAllowedSyncshells = new();
|
||||||
|
private string _mcdfShareIndividualDropdownSelection = string.Empty;
|
||||||
|
private string _mcdfShareIndividualInput = string.Empty;
|
||||||
|
private string _mcdfShareSyncshellDropdownSelection = string.Empty;
|
||||||
|
private string _mcdfShareSyncshellInput = string.Empty;
|
||||||
|
private int _mcdfShareExpireDays;
|
||||||
|
|
||||||
public CharaDataHubUi(ILogger<CharaDataHubUi> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
|
public CharaDataHubUi(ILogger<CharaDataHubUi> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
|
||||||
CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService,
|
CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService,
|
||||||
UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager,
|
UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager,
|
||||||
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager,
|
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager,
|
||||||
CharaDataGposeTogetherManager charaDataGposeTogetherManager)
|
CharaDataGposeTogetherManager charaDataGposeTogetherManager, McdfShareManager mcdfShareManager)
|
||||||
: base(logger, mediator, "Umbra Character Data Hub###UmbraCharaDataUI", performanceCollectorService)
|
: base(logger, mediator, "Umbra Character Data Hub###UmbraCharaDataUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
SetWindowSizeConstraints();
|
SetWindowSizeConstraints();
|
||||||
@@ -92,6 +116,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
_fileDialogManager = fileDialogManager;
|
_fileDialogManager = fileDialogManager;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_charaDataGposeTogetherManager = charaDataGposeTogetherManager;
|
_charaDataGposeTogetherManager = charaDataGposeTogetherManager;
|
||||||
|
_mcdfShareManager = mcdfShareManager;
|
||||||
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen |= _configService.Current.OpenMareHubOnGposeStart);
|
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen |= _configService.Current.OpenMareHubOnGposeStart);
|
||||||
Mediator.Subscribe<OpenCharaDataHubWithFilterMessage>(this, (msg) =>
|
Mediator.Subscribe<OpenCharaDataHubWithFilterMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
@@ -123,7 +148,14 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_closalCts.Cancel();
|
try
|
||||||
|
{
|
||||||
|
_closalCts?.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
EnsureFreshCts(ref _closalCts);
|
||||||
SelectedDtoId = string.Empty;
|
SelectedDtoId = string.Empty;
|
||||||
_filteredDict = null;
|
_filteredDict = null;
|
||||||
_sharedWithYouOwnerFilter = string.Empty;
|
_sharedWithYouOwnerFilter = string.Empty;
|
||||||
@@ -135,21 +167,34 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
public override void OnOpen()
|
public override void OnOpen()
|
||||||
{
|
{
|
||||||
_closalCts = _closalCts.CancelRecreate();
|
EnsureFreshCts(ref _closalCts);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_closalCts.CancelDispose();
|
CancelAndDispose(ref _closalCts);
|
||||||
_disposalCts.CancelDispose();
|
CancelAndDispose(ref _disposalCts);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
DrawHubContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawInline()
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId("CharaDataHubInline"))
|
||||||
|
{
|
||||||
|
DrawHubContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHubContent()
|
||||||
{
|
{
|
||||||
if (!_comboHybridUsedLastFrame)
|
if (!_comboHybridUsedLastFrame)
|
||||||
{
|
{
|
||||||
@@ -170,7 +215,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
if (!_charaDataManager.BrioAvailable)
|
if (!_charaDataManager.BrioAvailable)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(3);
|
ImGuiHelpers.ScaledDummy(3);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", ImGuiColors.DalamudRed);
|
UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", UiSharedService.AccentColor);
|
||||||
UiSharedService.DistanceSeparator();
|
UiSharedService.DistanceSeparator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,125 +235,150 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
if (!string.IsNullOrEmpty(_charaDataManager.DataApplicationProgress))
|
if (!string.IsNullOrEmpty(_charaDataManager.DataApplicationProgress))
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
if (_charaDataManager.DataApplicationTask != null)
|
if (_charaDataManager.DataApplicationTask != null)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("WARNING: During the data application avoid interacting with this actor to prevent potential crashes.", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("WARNING: During the data application avoid interacting with this actor to prevent potential crashes.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
using var tabs = ImRaii.TabBar("TabsTopLevel");
|
|
||||||
bool smallUi = false;
|
bool smallUi = false;
|
||||||
|
using (var topTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
|
||||||
_isHandlingSelf = _charaDataManager.HandledCharaData.Any(c => c.Value.IsSelf);
|
using (var topTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
|
||||||
if (_isHandlingSelf) _openMcdOnlineOnNextRun = false;
|
using (var topTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
|
||||||
|
|
||||||
using (var gposeTogetherTabItem = ImRaii.TabItem("GPose Together"))
|
|
||||||
{
|
{
|
||||||
if (gposeTogetherTabItem)
|
using var tabs = ImRaii.TabBar("TabsTopLevel");
|
||||||
|
|
||||||
|
_isHandlingSelf = _charaDataManager.HandledCharaData.Any(c => c.Value.IsSelf);
|
||||||
|
if (_isHandlingSelf) _openMcdOnlineOnNextRun = false;
|
||||||
|
|
||||||
|
using (var gposeTogetherTabItem = ImRaii.TabItem("GPose Together"))
|
||||||
{
|
{
|
||||||
smallUi = true;
|
if (gposeTogetherTabItem)
|
||||||
|
|
||||||
DrawGposeTogether();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var applicationTabItem = ImRaii.TabItem("Data Application", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
|
||||||
{
|
|
||||||
if (applicationTabItem)
|
|
||||||
{
|
|
||||||
smallUi = true;
|
|
||||||
using var appTabs = ImRaii.TabBar("TabsApplicationLevel");
|
|
||||||
|
|
||||||
using (ImRaii.Disabled(!_uiSharedService.IsInGpose))
|
|
||||||
{
|
{
|
||||||
using (var gposeTabItem = ImRaii.TabItem("GPose Actors"))
|
smallUi = true;
|
||||||
|
|
||||||
|
DrawGposeTogether();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var applicationTabItem = ImRaii.TabItem("Data Application", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
||||||
|
{
|
||||||
|
if (applicationTabItem)
|
||||||
|
{
|
||||||
|
smallUi = true;
|
||||||
|
using (var appTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
|
||||||
|
using (var appTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
|
||||||
|
using (var appTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
|
||||||
{
|
{
|
||||||
if (gposeTabItem)
|
using var appTabs = ImRaii.TabBar("TabsApplicationLevel");
|
||||||
|
|
||||||
|
using (ImRaii.Disabled(!_uiSharedService.IsInGpose))
|
||||||
{
|
{
|
||||||
using var id = ImRaii.PushId("gposeControls");
|
using (var gposeTabItem = ImRaii.TabItem("GPose Actors"))
|
||||||
DrawGposeControls();
|
{
|
||||||
|
if (gposeTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("gposeControls");
|
||||||
|
DrawGposeControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!_uiSharedService.IsInGpose)
|
||||||
|
UiSharedService.AttachToolTip("Only available in GPose");
|
||||||
|
|
||||||
|
using (var nearbyPosesTabItem = ImRaii.TabItem("Poses Nearby"))
|
||||||
|
{
|
||||||
|
if (nearbyPosesTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("nearbyPoseControls");
|
||||||
|
_charaDataNearbyManager.ComputeNearbyData = true;
|
||||||
|
|
||||||
|
DrawNearbyPoses();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_charaDataNearbyManager.ComputeNearbyData = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var gposeTabItem = ImRaii.TabItem("Apply Data", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
||||||
|
{
|
||||||
|
if (gposeTabItem)
|
||||||
|
{
|
||||||
|
smallUi |= true;
|
||||||
|
using var id = ImRaii.PushId("applyData");
|
||||||
|
DrawDataApplication();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!_uiSharedService.IsInGpose)
|
else
|
||||||
UiSharedService.AttachToolTip("Only available in GPose");
|
|
||||||
|
|
||||||
using (var nearbyPosesTabItem = ImRaii.TabItem("Poses Nearby"))
|
|
||||||
{
|
{
|
||||||
if (nearbyPosesTabItem)
|
_charaDataNearbyManager.ComputeNearbyData = false;
|
||||||
{
|
|
||||||
using var id = ImRaii.PushId("nearbyPoseControls");
|
|
||||||
_charaDataNearbyManager.ComputeNearbyData = true;
|
|
||||||
|
|
||||||
DrawNearbyPoses();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_charaDataNearbyManager.ComputeNearbyData = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var gposeTabItem = ImRaii.TabItem("Apply Data", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
|
||||||
{
|
|
||||||
if (gposeTabItem)
|
|
||||||
{
|
|
||||||
smallUi |= true;
|
|
||||||
using var id = ImRaii.PushId("applyData");
|
|
||||||
DrawDataApplication();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
_charaDataNearbyManager.ComputeNearbyData = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (ImRaii.Disabled(_isHandlingSelf))
|
using (ImRaii.Disabled(_isHandlingSelf))
|
||||||
{
|
|
||||||
ImGuiTabItemFlags flagsTopLevel = ImGuiTabItemFlags.None;
|
|
||||||
if (_openMcdOnlineOnNextRun)
|
|
||||||
{
|
{
|
||||||
flagsTopLevel = ImGuiTabItemFlags.SetSelected;
|
ImGuiTabItemFlags flagsTopLevel = ImGuiTabItemFlags.None;
|
||||||
_openMcdOnlineOnNextRun = false;
|
if (_openMcdOnlineOnNextRun)
|
||||||
}
|
|
||||||
|
|
||||||
using (var creationTabItem = ImRaii.TabItem("Data Creation", flagsTopLevel))
|
|
||||||
{
|
|
||||||
if (creationTabItem)
|
|
||||||
{
|
{
|
||||||
using var creationTabs = ImRaii.TabBar("TabsCreationLevel");
|
flagsTopLevel = ImGuiTabItemFlags.SetSelected;
|
||||||
|
_openMcdOnlineOnNextRun = false;
|
||||||
|
}
|
||||||
|
|
||||||
ImGuiTabItemFlags flags = ImGuiTabItemFlags.None;
|
using (var creationTabItem = ImRaii.TabItem("Data Creation", flagsTopLevel))
|
||||||
if (_openMcdOnlineOnNextRun)
|
{
|
||||||
|
if (creationTabItem)
|
||||||
{
|
{
|
||||||
flags = ImGuiTabItemFlags.SetSelected;
|
using (var creationTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
|
||||||
_openMcdOnlineOnNextRun = false;
|
using (var creationTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
|
||||||
}
|
using (var creationTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
|
||||||
using (var mcdOnlineTabItem = ImRaii.TabItem("Online Data", flags))
|
|
||||||
{
|
|
||||||
if (mcdOnlineTabItem)
|
|
||||||
{
|
{
|
||||||
using var id = ImRaii.PushId("mcdOnline");
|
using var creationTabs = ImRaii.TabBar("TabsCreationLevel");
|
||||||
DrawMcdOnline();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var mcdfTabItem = ImRaii.TabItem("MCDF Export"))
|
ImGuiTabItemFlags flags = ImGuiTabItemFlags.None;
|
||||||
{
|
if (_openMcdOnlineOnNextRun)
|
||||||
if (mcdfTabItem)
|
{
|
||||||
{
|
flags = ImGuiTabItemFlags.SetSelected;
|
||||||
using var id = ImRaii.PushId("mcdfExport");
|
_openMcdOnlineOnNextRun = false;
|
||||||
DrawMcdfExport();
|
}
|
||||||
|
using (var mcdOnlineTabItem = ImRaii.TabItem("Online Data", flags))
|
||||||
|
{
|
||||||
|
if (mcdOnlineTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("mcdOnline");
|
||||||
|
DrawMcdOnline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var mcdfTabItem = ImRaii.TabItem("MCDF Export"))
|
||||||
|
{
|
||||||
|
if (mcdfTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("mcdfExport");
|
||||||
|
DrawMcdfExport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var mcdfShareTabItem = ImRaii.TabItem("Partage MCDF"))
|
||||||
|
{
|
||||||
|
if (mcdfShareTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("mcdfShare");
|
||||||
|
DrawMcdfShare();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isHandlingSelf)
|
if (_isHandlingSelf)
|
||||||
{
|
{
|
||||||
UiSharedService.AttachToolTip("Cannot use creation tools while having Character Data applied to self.");
|
UiSharedService.AttachToolTip("Cannot use creation tools while having Character Data applied to self.");
|
||||||
@@ -436,14 +506,18 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
if (!_hasValidGposeTarget)
|
if (!_hasValidGposeTarget)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(3);
|
ImGuiHelpers.ScaledDummy(3);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", ImGuiColors.DalamudYellow, 350);
|
UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", UiSharedService.AccentColor, 350);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10);
|
ImGuiHelpers.ScaledDummy(10);
|
||||||
|
|
||||||
using var tabs = ImRaii.TabBar("Tabs");
|
using (var applyTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
|
||||||
|
using (var applyTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
|
||||||
|
using (var applyTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
|
||||||
|
{
|
||||||
|
using var tabs = ImRaii.TabBar("Tabs");
|
||||||
|
|
||||||
using (var byFavoriteTabItem = ImRaii.TabItem("Favorites"))
|
using (var byFavoriteTabItem = ImRaii.TabItem("Favorites"))
|
||||||
{
|
{
|
||||||
if (byFavoriteTabItem)
|
if (byFavoriteTabItem)
|
||||||
{
|
{
|
||||||
@@ -595,7 +669,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if (_configService.Current.FavoriteCodes.Count == 0)
|
if (_configService.Current.FavoriteCodes.Count == 0)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -644,11 +718,11 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
if (!_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false)
|
if (!_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success)
|
if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped(_charaDataManager.DownloadMetaInfoTask.Result.Result, ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped(_charaDataManager.DownloadMetaInfoTask.Result.Result, UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (ImRaii.Disabled(_charaDataManager.LastDownloadedMetaInfo == null))
|
using (ImRaii.Disabled(_charaDataManager.LastDownloadedMetaInfo == null))
|
||||||
@@ -689,7 +763,8 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Character Data"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Character Data"))
|
||||||
{
|
{
|
||||||
_ = _charaDataManager.GetAllData(_disposalCts.Token);
|
var cts = EnsureFreshCts(ref _disposalCts);
|
||||||
|
_ = _charaDataManager.GetAllData(cts.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
|
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
|
||||||
@@ -848,17 +923,18 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
if ((_charaDataManager.LoadedMcdfHeader?.IsFaulted ?? false) || (_charaDataManager.McdfApplicationTask?.IsFaulted ?? false))
|
if ((_charaDataManager.LoadedMcdfHeader?.IsFaulted ?? false) || (_charaDataManager.McdfApplicationTask?.IsFaulted ?? false))
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
|
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
|
||||||
ImGuiColors.DalamudRed);
|
UiSharedService.AccentColor);
|
||||||
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
|
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
|
||||||
"If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow);
|
"If you received it from someone else have them do the same.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("Loading Character...", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMcdfExport()
|
private void DrawMcdfExport()
|
||||||
@@ -883,7 +959,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
string defaultFileName = string.IsNullOrEmpty(_exportDescription)
|
string defaultFileName = string.IsNullOrEmpty(_exportDescription)
|
||||||
? "export.mcdf"
|
? "export.mcdf"
|
||||||
: string.Join('_', $"{_exportDescription}.mcdf".Split(Path.GetInvalidFileNameChars()));
|
: SanitizeFileName(_exportDescription, "export") + ".mcdf";
|
||||||
_uiSharedService.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", defaultFileName, ".mcdf", (success, path) =>
|
_uiSharedService.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", defaultFileName, ".mcdf", (success, path) =>
|
||||||
{
|
{
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
@@ -896,12 +972,469 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
}, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null);
|
}, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null);
|
||||||
}
|
}
|
||||||
UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" +
|
UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" +
|
||||||
" equipped and redraw your character before exporting.", ImGuiColors.DalamudYellow);
|
" equipped and redraw your character before exporting.", UiSharedService.AccentColor);
|
||||||
|
|
||||||
ImGui.Unindent();
|
ImGui.Unindent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawMcdfShare()
|
||||||
|
{
|
||||||
|
if (!_mcdfShareInitialized && !_mcdfShareManager.IsBusy)
|
||||||
|
{
|
||||||
|
_mcdfShareInitialized = true;
|
||||||
|
_ = _mcdfShareManager.RefreshAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_mcdfShareManager.IsBusy)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Traitement en cours...", ImGuiColors.DalamudYellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_mcdfShareManager.LastError))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(_mcdfShareManager.LastError!, ImGuiColors.DalamudRed);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(_mcdfShareManager.LastSuccess))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(_mcdfShareManager.LastSuccess!, ImGuiColors.HealerGreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Button("Actualiser les partages"))
|
||||||
|
{
|
||||||
|
_ = _mcdfShareManager.RefreshAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
_uiSharedService.BigText("Créer un partage MCDF");
|
||||||
|
|
||||||
|
ImGui.InputTextWithHint("##mcdfShareDescription", "Description", ref _mcdfShareDescription, 128);
|
||||||
|
ImGui.InputInt("Expiration (jours, 0 = jamais)", ref _mcdfShareExpireDays);
|
||||||
|
|
||||||
|
DrawMcdfShareIndividualDropdown();
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetNextItemWidth(220f);
|
||||||
|
if (ImGui.InputTextWithHint("##mcdfShareUidInput", "UID ou vanity", ref _mcdfShareIndividualInput, 32))
|
||||||
|
{
|
||||||
|
_mcdfShareIndividualDropdownSelection = string.Empty;
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
var normalizedUid = NormalizeUidCandidate(_mcdfShareIndividualInput);
|
||||||
|
using (ImRaii.Disabled(string.IsNullOrEmpty(normalizedUid)
|
||||||
|
|| _mcdfShareAllowedIndividuals.Any(p => string.Equals(p, normalizedUid, StringComparison.OrdinalIgnoreCase))))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("Ajouter"))
|
||||||
|
{
|
||||||
|
_mcdfShareAllowedIndividuals.Add(normalizedUid);
|
||||||
|
_mcdfShareIndividualInput = string.Empty;
|
||||||
|
_mcdfShareIndividualDropdownSelection = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("UID synchronisé à ajouter");
|
||||||
|
_uiSharedService.DrawHelpText("Choisissez un pair synchronisé dans la liste ou saisissez un UID. Les utilisateurs listés pourront récupérer ce partage MCDF.");
|
||||||
|
|
||||||
|
foreach (var uid in _mcdfShareAllowedIndividuals.ToArray())
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId("mcdfShareUid" + uid))
|
||||||
|
{
|
||||||
|
ImGui.BulletText(FormatPairLabel(uid));
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Retirer"))
|
||||||
|
{
|
||||||
|
_mcdfShareAllowedIndividuals.Remove(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawMcdfShareSyncshellDropdown();
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetNextItemWidth(220f);
|
||||||
|
if (ImGui.InputTextWithHint("##mcdfShareSyncshellInput", "GID ou alias", ref _mcdfShareSyncshellInput, 32))
|
||||||
|
{
|
||||||
|
_mcdfShareSyncshellDropdownSelection = string.Empty;
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
var normalizedSyncshell = NormalizeSyncshellCandidate(_mcdfShareSyncshellInput);
|
||||||
|
using (ImRaii.Disabled(string.IsNullOrEmpty(normalizedSyncshell)
|
||||||
|
|| _mcdfShareAllowedSyncshells.Any(p => string.Equals(p, normalizedSyncshell, StringComparison.OrdinalIgnoreCase))))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("Ajouter"))
|
||||||
|
{
|
||||||
|
_mcdfShareAllowedSyncshells.Add(normalizedSyncshell);
|
||||||
|
_mcdfShareSyncshellInput = string.Empty;
|
||||||
|
_mcdfShareSyncshellDropdownSelection = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("Syncshell à ajouter");
|
||||||
|
_uiSharedService.DrawHelpText("Sélectionnez une syncshell synchronisée ou saisissez un identifiant. Les syncshells listées auront accès au partage.");
|
||||||
|
|
||||||
|
foreach (var shell in _mcdfShareAllowedSyncshells.ToArray())
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId("mcdfShareShell" + shell))
|
||||||
|
{
|
||||||
|
ImGui.BulletText(FormatSyncshellLabel(shell));
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Retirer"))
|
||||||
|
{
|
||||||
|
_mcdfShareAllowedSyncshells.Remove(shell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (ImRaii.Disabled(_mcdfShareManager.IsBusy))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Créer"))
|
||||||
|
{
|
||||||
|
DateTime? expiresAt = _mcdfShareExpireDays <= 0 ? null : DateTime.UtcNow.AddDays(_mcdfShareExpireDays);
|
||||||
|
_ = _mcdfShareManager.CreateShareAsync(_mcdfShareDescription, _mcdfShareAllowedIndividuals.ToList(), _mcdfShareAllowedSyncshells.ToList(), expiresAt, CancellationToken.None);
|
||||||
|
_mcdfShareDescription = string.Empty;
|
||||||
|
_mcdfShareAllowedIndividuals.Clear();
|
||||||
|
_mcdfShareAllowedSyncshells.Clear();
|
||||||
|
_mcdfShareIndividualInput = string.Empty;
|
||||||
|
_mcdfShareIndividualDropdownSelection = string.Empty;
|
||||||
|
_mcdfShareSyncshellInput = string.Empty;
|
||||||
|
_mcdfShareSyncshellDropdownSelection = string.Empty;
|
||||||
|
_mcdfShareExpireDays = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
_uiSharedService.BigText("Mes partages : ");
|
||||||
|
|
||||||
|
if (_mcdfShareManager.OwnShares.Count == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucun partage MCDF créé.");
|
||||||
|
}
|
||||||
|
else if (ImGui.BeginTable("mcdf-own-shares", 6, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter))
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("Description");
|
||||||
|
ImGui.TableSetupColumn("Créé le");
|
||||||
|
ImGui.TableSetupColumn("Expire");
|
||||||
|
ImGui.TableSetupColumn("Téléchargements");
|
||||||
|
ImGui.TableSetupColumn("Accès");
|
||||||
|
var style = ImGui.GetStyle();
|
||||||
|
float BtnWidth(string label) => ImGui.CalcTextSize(label).X + style.FramePadding.X * 2f;
|
||||||
|
float ownActionsWidth = BtnWidth("Appliquer en GPose") + style.ItemSpacing.X + BtnWidth("Enregistrer") + style.ItemSpacing.X + BtnWidth("Supprimer") + 2f; // small margin
|
||||||
|
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, ownActionsWidth);
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
foreach (var entry in _mcdfShareManager.OwnShares)
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.Description) ? entry.Id.ToString() : entry.Description);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.CreatedUtc.ToLocalTime().ToString("g"));
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.ExpiresAtUtc.HasValue ? entry.ExpiresAtUtc.Value.ToLocalTime().ToString("g") : "Jamais");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.DownloadCount.ToString());
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted($"UID : {entry.AllowedIndividuals.Count}, Syncshells : {entry.AllowedSyncshells.Count}");
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.BeginTooltip();
|
||||||
|
if (entry.AllowedIndividuals.Count > 0)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("UID autorisés:");
|
||||||
|
foreach (var uid in entry.AllowedIndividuals)
|
||||||
|
ImGui.BulletText(FormatUidWithName(uid));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucun UID autorisé");
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
if (entry.AllowedSyncshells.Count > 0)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Syncshells autorisées:");
|
||||||
|
foreach (var gid in entry.AllowedSyncshells)
|
||||||
|
ImGui.BulletText(FormatSyncshellLabel(gid));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucune syncshell autorisée");
|
||||||
|
}
|
||||||
|
ImGui.EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.PushId("ownShare" + entry.Id))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("Appliquer en GPose"))
|
||||||
|
{
|
||||||
|
_ = _mcdfShareManager.ApplyShareAsync(entry.Id, CancellationToken.None);
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Enregistrer"))
|
||||||
|
{
|
||||||
|
var baseName = SanitizeFileName(entry.Description, entry.Id.ToString());
|
||||||
|
var defaultName = baseName + ".mcdf";
|
||||||
|
_fileDialogManager.SaveFileDialog("Enregistrer le partage MCDF", ".mcdf", defaultName, ".mcdf", async (success, path) =>
|
||||||
|
{
|
||||||
|
if (!success || string.IsNullOrEmpty(path)) return;
|
||||||
|
await _mcdfShareManager.ExportShareAsync(entry.Id, path, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Supprimer"))
|
||||||
|
{
|
||||||
|
_ = _mcdfShareManager.DeleteShareAsync(entry.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
_uiSharedService.BigText("Partagés avec moi : ");
|
||||||
|
|
||||||
|
if (_mcdfShareManager.SharedShares.Count == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucun partage MCDF reçu.");
|
||||||
|
}
|
||||||
|
else if (ImGui.BeginTable("mcdf-shared-shares", 5, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter))
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("Description");
|
||||||
|
ImGui.TableSetupColumn("Propriétaire");
|
||||||
|
ImGui.TableSetupColumn("Expire");
|
||||||
|
ImGui.TableSetupColumn("Téléchargements");
|
||||||
|
var style2 = ImGui.GetStyle();
|
||||||
|
float BtnWidth2(string label) => ImGui.CalcTextSize(label).X + style2.FramePadding.X * 2f;
|
||||||
|
float sharedActionsWidth = BtnWidth2("Appliquer") + style2.ItemSpacing.X + BtnWidth2("Enregistrer") + 2f;
|
||||||
|
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, sharedActionsWidth);
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
foreach (var entry in _mcdfShareManager.SharedShares)
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.Description) ? entry.Id.ToString() : entry.Description);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.OwnerAlias) ? entry.OwnerUid : entry.OwnerAlias);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.BeginTooltip();
|
||||||
|
ImGui.TextUnformatted($"UID propriétaire: {entry.OwnerUid}");
|
||||||
|
if (!string.IsNullOrEmpty(entry.OwnerAlias))
|
||||||
|
{
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.TextUnformatted($"Alias: {entry.OwnerAlias}");
|
||||||
|
}
|
||||||
|
ImGui.EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.ExpiresAtUtc.HasValue ? entry.ExpiresAtUtc.Value.ToLocalTime().ToString("g") : "Jamais");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.DownloadCount.ToString());
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.PushId("sharedShare" + entry.Id))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("Appliquer"))
|
||||||
|
{
|
||||||
|
_ = _mcdfShareManager.ApplyShareAsync(entry.Id, CancellationToken.None);
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Enregistrer"))
|
||||||
|
{
|
||||||
|
var baseName = SanitizeFileName(entry.Description, entry.Id.ToString());
|
||||||
|
var defaultName = baseName + ".mcdf";
|
||||||
|
_fileDialogManager.SaveFileDialog("Enregistrer le partage MCDF", ".mcdf", defaultName, ".mcdf", async (success, path) =>
|
||||||
|
{
|
||||||
|
if (!success || string.IsNullOrEmpty(path)) return;
|
||||||
|
await _mcdfShareManager.ExportShareAsync(entry.Id, path, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMcdfShareIndividualDropdown()
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(220f);
|
||||||
|
var previewSource = string.IsNullOrEmpty(_mcdfShareIndividualDropdownSelection)
|
||||||
|
? _mcdfShareIndividualInput
|
||||||
|
: _mcdfShareIndividualDropdownSelection;
|
||||||
|
var previewLabel = string.IsNullOrEmpty(previewSource)
|
||||||
|
? "Sélectionner un pair synchronisé..."
|
||||||
|
: FormatPairLabel(previewSource);
|
||||||
|
|
||||||
|
using var combo = ImRaii.Combo("##mcdfShareUidDropdown", previewLabel, ImGuiComboFlags.None);
|
||||||
|
if (!combo)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pair in _pairManager.DirectPairs
|
||||||
|
.OrderBy(p => p.GetNoteOrName() ?? p.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var normalized = pair.UserData.UID;
|
||||||
|
var display = FormatPairLabel(normalized);
|
||||||
|
bool selected = string.Equals(normalized, _mcdfShareIndividualDropdownSelection, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (ImGui.Selectable(display, selected))
|
||||||
|
{
|
||||||
|
_mcdfShareIndividualDropdownSelection = normalized;
|
||||||
|
_mcdfShareIndividualInput = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMcdfShareSyncshellDropdown()
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(220f);
|
||||||
|
var previewSource = string.IsNullOrEmpty(_mcdfShareSyncshellDropdownSelection)
|
||||||
|
? _mcdfShareSyncshellInput
|
||||||
|
: _mcdfShareSyncshellDropdownSelection;
|
||||||
|
var previewLabel = string.IsNullOrEmpty(previewSource)
|
||||||
|
? "Sélectionner une syncshell..."
|
||||||
|
: FormatSyncshellLabel(previewSource);
|
||||||
|
|
||||||
|
using var combo = ImRaii.Combo("##mcdfShareSyncshellDropdown", previewLabel, ImGuiComboFlags.None);
|
||||||
|
if (!combo)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in _pairManager.Groups.Values
|
||||||
|
.OrderBy(g => _serverConfigurationManager.GetNoteForGid(g.GID) ?? g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var gid = group.GID;
|
||||||
|
var display = FormatSyncshellLabel(gid);
|
||||||
|
bool selected = string.Equals(gid, _mcdfShareSyncshellDropdownSelection, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (ImGui.Selectable(display, selected))
|
||||||
|
{
|
||||||
|
_mcdfShareSyncshellDropdownSelection = gid;
|
||||||
|
_mcdfShareSyncshellInput = gid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeUidCandidate(string candidate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = candidate.Trim();
|
||||||
|
|
||||||
|
foreach (var pair in _pairManager.DirectPairs)
|
||||||
|
{
|
||||||
|
var alias = pair.UserData.Alias;
|
||||||
|
var aliasOrUid = pair.UserData.AliasOrUID;
|
||||||
|
var note = pair.GetNoteOrName();
|
||||||
|
|
||||||
|
if (string.Equals(pair.UserData.UID, trimmed, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, trimmed, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(aliasOrUid, trimmed, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(note) && string.Equals(note, trimmed, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return pair.UserData.UID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeSyncshellCandidate(string candidate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = candidate.Trim();
|
||||||
|
|
||||||
|
foreach (var group in _pairManager.Groups.Values)
|
||||||
|
{
|
||||||
|
var alias = group.GroupAlias;
|
||||||
|
var aliasOrGid = group.GroupAliasOrGID;
|
||||||
|
var note = _serverConfigurationManager.GetNoteForGid(group.GID);
|
||||||
|
|
||||||
|
if (string.Equals(group.GID, trimmed, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, trimmed, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(aliasOrGid, trimmed, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(note) && string.Equals(note, trimmed, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return group.GID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatUidWithName(string uid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(uid)) return string.Empty;
|
||||||
|
var note = _serverConfigurationManager.GetNoteForUid(uid);
|
||||||
|
if (!string.IsNullOrEmpty(note)) return $"{uid} ({note})";
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatPairLabel(string candidate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(candidate))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pair in _pairManager.DirectPairs)
|
||||||
|
{
|
||||||
|
var alias = pair.UserData.Alias;
|
||||||
|
var aliasOrUid = pair.UserData.AliasOrUID;
|
||||||
|
var note = pair.GetNoteOrName();
|
||||||
|
|
||||||
|
if (string.Equals(pair.UserData.UID, candidate, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, candidate, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(aliasOrUid, candidate, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(note) && string.Equals(note, candidate, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(note) ? aliasOrUid : $"{note} ({aliasOrUid})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatSyncshellLabel(string candidate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(candidate))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in _pairManager.Groups.Values)
|
||||||
|
{
|
||||||
|
var alias = group.GroupAlias;
|
||||||
|
var aliasOrGid = group.GroupAliasOrGID;
|
||||||
|
var note = _serverConfigurationManager.GetNoteForGid(group.GID);
|
||||||
|
|
||||||
|
if (string.Equals(group.GID, candidate, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, candidate, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(aliasOrGid, candidate, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(note) && string.Equals(note, candidate, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(note) ? aliasOrGid : $"{note} ({aliasOrGid})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawMetaInfoData(string selectedGposeActor, bool hasValidGposeTarget, CharaDataMetaInfoExtendedDto data, bool canOpen = false)
|
private void DrawMetaInfoData(string selectedGposeActor, bool hasValidGposeTarget, CharaDataMetaInfoExtendedDto data, bool canOpen = false)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
@@ -1104,4 +1637,26 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
drawAction();
|
drawAction();
|
||||||
if (_disableUI) ImGui.BeginDisabled();
|
if (_disableUI) ImGui.BeginDisabled();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user