Re-add performance thresholds and add whitelist/blacklist options

This commit is contained in:
Loporrit
2025-02-20 16:05:29 +00:00
parent 7918b54c92
commit 387aacdd6a
17 changed files with 704 additions and 64 deletions

View File

@@ -96,8 +96,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
public void HaltScan(string source) public void HaltScan(string source)
{ {
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0; HaltScanLocks.AddOrUpdate(source, 1, (k, v) => v + 1);
HaltScanLocks[source]++;
} }
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null); record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
@@ -447,10 +446,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
public void ResumeScan(string source) public void ResumeScan(string source)
{ {
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0; HaltScanLocks.AddOrUpdate(source, 0, (k, v) => Math.Max(0, v - 1));
HaltScanLocks[source]--;
if (HaltScanLocks[source] < 0) HaltScanLocks[source] = 0;
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)

View File

@@ -19,7 +19,8 @@ public class MareConfig : IMareConfiguration
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: 0xFFBA47u);
public bool UseNameColors { get; set; } = false; public bool UseNameColors { get; set; } = false;
public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0xF5EB67u, Glow: 0x78710Fu); public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu);
public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u);
public bool EnableRightClickMenus { get; set; } = true; public bool EnableRightClickMenus { get; set; } = true;
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
public string ExportFolder { get; set; } = string.Empty; public string ExportFolder { get; set; } = string.Empty;

View File

@@ -0,0 +1,12 @@
namespace MareSynchronos.MareConfiguration.Configurations;
public class PlayerPerformanceConfig : IMareConfiguration
{
public int Version { get; set; } = 1;
public bool AutoPausePlayersExceedingThresholds { get; set; } = false;
public bool NotifyAutoPauseDirectPairs { get; set; } = true;
public bool NotifyAutoPauseGroupPairs { get; set; } = false;
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 550;
public int TrisAutoPauseThresholdThousands { get; set; } = 250;
public bool IgnoreDirectPairs { get; set; } = true;
}

View File

@@ -0,0 +1,10 @@
using MareSynchronos.MareConfiguration.Models;
namespace MareSynchronos.MareConfiguration.Configurations;
[Serializable]
public class ServerBlockConfig : IMareConfiguration
{
public Dictionary<string, ServerBlockStorage> ServerBlocks { get; set; } = new(StringComparer.Ordinal);
public int Version { get; set; } = 0;
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronos.MareConfiguration.Models;
[Serializable]
public class ServerBlockStorage
{
public List<string> Whitelist { get; set; } = new();
public List<string> Blacklist { get; set; } = new();
}

View File

@@ -0,0 +1,11 @@
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class PlayerPerformanceConfigService : ConfigurationServiceBase<PlayerPerformanceConfig>
{
public const string ConfigName = "playerperformance.json";
public PlayerPerformanceConfigService(string configDir) : base(configDir) { }
protected override string ConfigurationName => ConfigName;
}

View File

@@ -0,0 +1,14 @@
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class ServerBlockConfigService : ConfigurationServiceBase<ServerBlockConfig>
{
public const string ConfigName = "blocks.json";
public ServerBlockConfigService(string configDir) : base(configDir)
{
}
protected override string ConfigurationName => ConfigName;
}

View File

@@ -1,4 +1,5 @@
using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
@@ -25,8 +26,8 @@ public class PairFactory
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
} }
public Pair Create() public Pair Create(UserData userData)
{ {
return new Pair(_loggerFactory.CreateLogger<Pair>(), _cachedPlayerFactory, _mareMediator, _mareConfig, _serverConfigurationManager); return new Pair(_loggerFactory.CreateLogger<Pair>(), userData, _cachedPlayerFactory, _mareMediator, _mareConfig, _serverConfigurationManager);
} }
} }

View File

@@ -102,6 +102,12 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate(); _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate(); _applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
}); });
Mediator.Subscribe<RecalculatePerformanceMessage>(this, (msg) =>
{
if (msg.UID != null && !msg.UID.Equals(Pair.UserData.UID, StringComparison.Ordinal)) return;
Logger.LogDebug("Recalculating performance for {uid}", Pair.UserData.UID);
pair.ApplyLastReceivedData();
});
LastAppliedDataBytes = -1; LastAppliedDataBytes = -1;
} }
@@ -159,6 +165,21 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
SetUploading(isUploading: false); SetUploading(isUploading: false);
if (Pair.IsDownloadBlocked)
{
var reasons = string.Join(", ", Pair.HoldDownloadReasons);
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
$"Not applying character data: {reasons}")));
Logger.LogDebug("[BASE-{appBase}] Not applying due to hold: {reasons}", applicationBase, reasons);
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
this, forceApplyCustomization, forceApplyMods: false)
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
_forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null);
_cachedData = characterData;
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
return;
}
Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, this, forceApplyCustomization, _forceApplyMods); Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, this, forceApplyCustomization, _forceApplyMods);
Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA"); Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA");
@@ -220,6 +241,8 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
{ {
base.Dispose(disposing); base.Dispose(disposing);
if (!disposing) return;
SetUploading(isUploading: false); SetUploading(isUploading: false);
_downloadManager.Dispose(); _downloadManager.Dispose();
var name = PlayerName; var name = PlayerName;
@@ -227,18 +250,52 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
try try
{ {
Guid applicationId = Guid.NewGuid(); Guid applicationId = Guid.NewGuid();
_applicationCancellationTokenSource?.CancelDispose();
_applicationCancellationTokenSource = null;
_downloadCancellationTokenSource?.CancelDispose();
_downloadCancellationTokenSource = null;
_charaHandler?.Dispose();
_charaHandler = null;
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
{ {
Mediator.Publish(new EventMessage(new Event(name, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Disposing User"))); Mediator.Publish(new EventMessage(new Event(name, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Disposing User")));
} }
if (!_lifetime.ApplicationStopping.IsCancellationRequested)
UndoApplicationAsync(applicationId).GetAwaiter().GetResult();
_applicationCancellationTokenSource?.Dispose();
_applicationCancellationTokenSource = null;
_downloadCancellationTokenSource?.Dispose();
_downloadCancellationTokenSource = null;
_charaHandler?.Dispose();
_charaHandler = null;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error on disposal of {name}", name);
}
finally
{
PlayerName = null;
_cachedData = null;
Logger.LogDebug("Disposing {name} complete", name);
}
}
public void UndoApplication(Guid applicationId = default)
{
_ = Task.Run(async () => {
await UndoApplicationAsync(applicationId).ConfigureAwait(false);
});
}
private async Task UndoApplicationAsync(Guid applicationId = default)
{
Logger.LogDebug($"Undoing application of {Pair.UserPair}");
var name = PlayerName;
try
{
if (applicationId == default)
applicationId = Guid.NewGuid();
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, Pair.UserPair); Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, Pair.UserPair);
if (_penumbraCollection != Guid.Empty) if (_penumbraCollection != Guid.Empty)
{ {
@@ -246,15 +303,13 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_penumbraCollection = Guid.Empty; _penumbraCollection = Guid.Empty;
} }
if (_lifetime.ApplicationStopping.IsCancellationRequested) return;
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name)) if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
{ {
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, Pair.UserPair); Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, Pair.UserPair);
if (!IsVisible) if (!IsVisible)
{ {
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, Pair.UserPair); Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, Pair.UserPair);
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult(); await _ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId);
} }
else else
{ {
@@ -267,7 +322,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
{ {
try try
{ {
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult(); await RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token);
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
{ {
@@ -282,13 +337,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogWarning(ex, "Error on disposal of {name}", name); Logger.LogWarning(ex, "Error on undoing application of {name}", name);
}
finally
{
PlayerName = null;
_cachedData = null;
Logger.LogDebug("Disposing {name} complete", name);
} }
} }
@@ -382,16 +431,20 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource(); _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
var downloadToken = _downloadCancellationTokenSource.Token; var downloadToken = _downloadCancellationTokenSource.Token;
_ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false); _ = Task.Run(async () => {
await DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false);
});
} }
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken) bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
{ {
Logger.LogTrace("[BASE-{appBase}] DownloadAndApplyCharacterAsync", applicationBase);
Dictionary<(string GamePath, string? Hash), string> moddedPaths = []; Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
if (updateModdedPaths) if (updateModdedPaths)
{ {
Logger.LogTrace("[BASE-{appBase}] DownloadAndApplyCharacterAsync > updateModdedPaths", applicationBase);
int attempts = 0; int attempts = 0;
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
@@ -406,6 +459,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
{ {
Pair.HoldApplication("IndividualPerformanceThreshold", maxValue: 1);
_downloadManager.CancelDownload(); _downloadManager.CancelDownload();
return; return;
} }
@@ -427,11 +481,30 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
break; break;
} }
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
} }
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) bool exceedsThreshold = !await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false);
if (exceedsThreshold)
Pair.HoldApplication("IndividualPerformanceThreshold", maxValue: 1);
else
Pair.UnholdApplication("IndividualPerformanceThreshold");
if (exceedsThreshold)
{
Logger.LogTrace("[BASE-{appBase}] Not applying due to performance thresholds", applicationBase);
return; return;
}
}
if (Pair.IsApplicationBlocked)
{
var reasons = string.Join(", ", Pair.HoldApplicationReasons);
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
$"Not applying character data: {reasons}")));
Logger.LogTrace("[BASE-{appBase}] Not applying due to hold: {reasons}", applicationBase, reasons);
return;
} }
downloadToken.ThrowIfCancellationRequested(); downloadToken.ThrowIfCancellationRequested();
@@ -457,11 +530,20 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
private async Task ApplyCharacterDataAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip, private async Task ApplyCharacterDataAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token) Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
{ {
ushort objIndex = ushort.MaxValue;
try try
{ {
_applicationId = Guid.NewGuid(); _applicationId = Guid.NewGuid();
Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId); Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId);
if (_penumbraCollection == Guid.Empty)
{
if (objIndex == ushort.MaxValue)
objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false);
_penumbraCollection = await _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, Pair.UserData.UID);
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex);
}
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, _charaHandler); Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, _charaHandler);
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, _applicationId, 30000, token).ConfigureAwait(false); await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, _applicationId, 30000, token).ConfigureAwait(false);
@@ -470,7 +552,8 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
if (updateModdedPaths) if (updateModdedPaths)
{ {
// ensure collection is set // ensure collection is set
var objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false); if (objIndex == ushort.MaxValue)
objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false);
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false); await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false);
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection, await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection,
@@ -577,10 +660,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
Logger.LogTrace("Reapplying Pet Names data for {this}", this); Logger.LogTrace("Reapplying Pet Names data for {this}", this);
await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false); await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false);
}); });
if (_penumbraCollection == Guid.Empty)
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, Pair.UserData.UID).GetAwaiter().GetResult();
_ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, _charaHandler.GetGameObject()!.ObjectIndex).GetAwaiter().GetResult();
} }
private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken) private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken)

View File

@@ -12,28 +12,33 @@ 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.Collections.Concurrent;
namespace MareSynchronos.PlayerData.Pairs; namespace MareSynchronos.PlayerData.Pairs;
public class Pair public class Pair : DisposableMediatorSubscriberBase
{ {
private readonly PairHandlerFactory _cachedPlayerFactory; private readonly PairHandlerFactory _cachedPlayerFactory;
private readonly SemaphoreSlim _creationSemaphore = new(1); private readonly SemaphoreSlim _creationSemaphore = new(1);
private readonly ILogger<Pair> _logger; private readonly ILogger<Pair> _logger;
private readonly MareMediator _mediator;
private readonly MareConfigService _mareConfig; private readonly MareConfigService _mareConfig;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private CancellationTokenSource _applicationCts = new(); private CancellationTokenSource _applicationCts = new();
private OnlineUserIdentDto? _onlineUserIdentDto = null; private OnlineUserIdentDto? _onlineUserIdentDto = null;
public Pair(ILogger<Pair> logger, PairHandlerFactory cachedPlayerFactory, public Pair(ILogger<Pair> logger, UserData userData, PairHandlerFactory cachedPlayerFactory,
MareMediator mediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) MareMediator mediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager)
: base(logger, mediator)
{ {
_logger = logger; _logger = logger;
_cachedPlayerFactory = cachedPlayerFactory; _cachedPlayerFactory = cachedPlayerFactory;
_mediator = mediator;
_mareConfig = mareConfig; _mareConfig = mareConfig;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
UserData = userData;
Mediator.SubscribeKeyed<HoldPairApplicationMessage>(this, UserData.UID, (msg) => HoldApplication(msg.Source));
Mediator.SubscribeKeyed<UnholdPairApplicationMessage>(this, UserData.UID, (msg) => UnholdApplication(msg.Source));
} }
public Dictionary<GroupFullInfoDto, GroupPairFullInfoDto> GroupPair { get; set; } = new(GroupDtoComparer.Instance); public Dictionary<GroupFullInfoDto, GroupPairFullInfoDto> GroupPair { get; set; } = new(GroupDtoComparer.Instance);
@@ -43,6 +48,16 @@ public class Pair
public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused() public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused()
: GroupPair.All(p => p.Key.GroupUserPermissions.IsPaused() || p.Value.GroupUserPermissions.IsPaused()); : GroupPair.All(p => p.Key.GroupUserPermissions.IsPaused() || p.Value.GroupUserPermissions.IsPaused());
// Download locks apply earlier in the process than Application locks
private ConcurrentDictionary<string, int> HoldDownloadLocks { get; set; } = new(StringComparer.Ordinal);
private ConcurrentDictionary<string, int> HoldApplicationLocks { get; set; } = new(StringComparer.Ordinal);
public bool IsDownloadBlocked => HoldDownloadLocks.Any(f => f.Value > 0);
public bool IsApplicationBlocked => HoldApplicationLocks.Any(f => f.Value > 0) || IsDownloadBlocked;
public IEnumerable<string> HoldDownloadReasons => HoldDownloadLocks.Keys;
public IEnumerable<string> HoldApplicationReasons => Enumerable.Concat(HoldDownloadLocks.Keys, HoldApplicationLocks.Keys);
public bool IsVisible => CachedPlayer?.IsVisible ?? false; public bool IsVisible => CachedPlayer?.IsVisible ?? false;
public CharacterData? LastReceivedCharacterData { get; set; } public CharacterData? LastReceivedCharacterData { get; set; }
public string? PlayerName => GetPlayerName(); public string? PlayerName => GetPlayerName();
@@ -52,7 +67,7 @@ public class Pair
public long LastAppliedApproximateVRAMBytes { get; set; } = -1; public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty; public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty;
public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User; public UserData UserData { get; init; }
public UserPairDto? UserPair { get; set; } public UserPairDto? UserPair { get; set; }
@@ -65,30 +80,58 @@ public class Pair
args.AddMenuItem(new MenuItem() args.AddMenuItem(new MenuItem()
{ {
Name = "Open Profile", Name = "Open Profile",
OnClicked = (a) => _mediator.Publish(new ProfileOpenStandaloneMessage(this)), OnClicked = (a) => Mediator.Publish(new ProfileOpenStandaloneMessage(this)),
PrefixColor = 559, PrefixColor = 559,
PrefixChar = 'L' PrefixChar = 'L'
}); });
args.AddMenuItem(new MenuItem() if (!IsApplicationBlocked)
{ {
Name = "Reapply last data", args.AddMenuItem(new MenuItem()
OnClicked = (a) => ApplyLastReceivedData(forced: true), {
PrefixColor = 559, Name = "Always Block Modded Appearance",
PrefixChar = 'L', OnClicked = (a) => {
}); _serverConfigurationManager.AddBlacklistUid(UserData.UID);
HoldApplication("Blacklist", maxValue: 1);
},
PrefixColor = 559,
PrefixChar = 'L',
});
args.AddMenuItem(new MenuItem()
{
Name = "Reapply last data",
OnClicked = (a) => ApplyLastReceivedData(forced: true),
PrefixColor = 559,
PrefixChar = 'L',
});
}
else
{
args.AddMenuItem(new MenuItem()
{
Name = "Always Allow Modded Appearance",
OnClicked = (a) => {
_serverConfigurationManager.AddWhitelistUid(UserData.UID);
UnholdApplication("Blacklist", skipApplication: true);
// FIXME: Manually applying here should not be needed here if everything else was written properly
ApplyLastReceivedData(forced: true);
},
PrefixColor = 559,
PrefixChar = 'L',
});
}
if (UserPair != null) if (UserPair != null)
{ {
args.AddMenuItem(new MenuItem() args.AddMenuItem(new MenuItem()
{ {
Name = "Change Permissions", Name = "Change Permissions",
OnClicked = (a) => _mediator.Publish(new OpenPermissionWindow(this)), OnClicked = (a) => Mediator.Publish(new OpenPermissionWindow(this)),
PrefixColor = 559, PrefixColor = 559,
PrefixChar = 'L', PrefixChar = 'L',
}); });
args.AddMenuItem(new MenuItem() args.AddMenuItem(new MenuItem()
{ {
Name = "Cycle pause state", Name = "Cycle pause state",
OnClicked = (a) => _mediator.Publish(new CyclePauseMessage(UserData)), OnClicked = (a) => Mediator.Publish(new CyclePauseMessage(UserData)),
PrefixColor = 559, PrefixColor = 559,
PrefixChar = 'L', PrefixChar = 'L',
}); });
@@ -130,6 +173,10 @@ public class Pair
{ {
if (CachedPlayer == null) return; if (CachedPlayer == null) return;
if (LastReceivedCharacterData == null) return; if (LastReceivedCharacterData == null) return;
if (IsDownloadBlocked) return;
if (_serverConfigurationManager.IsUidBlacklisted(UserData.UID))
HoldApplication("Blacklist", maxValue: 1);
CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced); CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced);
} }
@@ -230,6 +277,44 @@ public class Pair
CachedPlayer?.SetUploading(); CachedPlayer?.SetUploading();
} }
public void HoldApplication(string source, int maxValue = int.MaxValue)
{
_logger.LogDebug($"Holding {UserData.UID} for reason: {source}");
bool wasHeld = IsApplicationBlocked;
HoldApplicationLocks.AddOrUpdate(source, 1, (k, v) => Math.Min(maxValue, v + 1));
if (!wasHeld)
CachedPlayer?.UndoApplication();
}
public void UnholdApplication(string source, bool skipApplication = false)
{
_logger.LogDebug($"Un-holding {UserData.UID} for reason: {source}");
bool wasHeld = IsApplicationBlocked;
HoldApplicationLocks.AddOrUpdate(source, 0, (k, v) => Math.Max(0, v - 1));
HoldApplicationLocks.TryRemove(new(source, 0));
if (!skipApplication && wasHeld && !IsApplicationBlocked)
ApplyLastReceivedData(forced: true);
}
public void HoldDownloads(string source, int maxValue = int.MaxValue)
{
_logger.LogDebug($"Holding {UserData.UID} for reason: {source}");
bool wasHeld = IsApplicationBlocked;
HoldDownloadLocks.AddOrUpdate(source, 1, (k, v) => Math.Min(maxValue, v + 1));
if (!wasHeld)
CachedPlayer?.UndoApplication();
}
public void UnholdDownloads(string source, bool skipApplication = false)
{
_logger.LogDebug($"Un-holding {UserData.UID} for reason: {source}");
bool wasHeld = IsApplicationBlocked;
HoldDownloadLocks.AddOrUpdate(source, 0, (k, v) => Math.Max(0, v - 1));
HoldDownloadLocks.TryRemove(new(source, 0));
if (!skipApplication && wasHeld && !IsApplicationBlocked)
ApplyLastReceivedData(forced: true);
}
private CharacterData? RemoveNotSyncedFiles(CharacterData? data) private CharacterData? RemoveNotSyncedFiles(CharacterData? data)
{ {
_logger.LogTrace("Removing not synced files"); _logger.LogTrace("Removing not synced files");

View File

@@ -54,7 +54,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
public void AddGroupPair(GroupPairFullInfoDto dto) public void AddGroupPair(GroupPairFullInfoDto dto)
{ {
if (!_allClientPairs.ContainsKey(dto.User)) if (!_allClientPairs.ContainsKey(dto.User))
_allClientPairs[dto.User] = _pairFactory.Create(); _allClientPairs[dto.User] = _pairFactory.Create(dto.User);
var group = _allGroups[dto.Group]; var group = _allGroups[dto.Group];
_allClientPairs[dto.User].GroupPair[group] = dto; _allClientPairs[dto.User].GroupPair[group] = dto;
@@ -65,7 +65,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
{ {
if (!_allClientPairs.ContainsKey(dto.User)) if (!_allClientPairs.ContainsKey(dto.User))
{ {
_allClientPairs[dto.User] = _pairFactory.Create(); _allClientPairs[dto.User] = _pairFactory.Create(dto.User);
} }
else else
{ {

View File

@@ -124,6 +124,8 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new SyncshellConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new SyncshellConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService<ILogger<ConfigurationMigrator>>(), s.GetRequiredService<NotesConfigService>())); collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService<ILogger<ConfigurationMigrator>>(), s.GetRequiredService<NotesConfigService>()));
collection.AddSingleton<HubFactory>(); collection.AddSingleton<HubFactory>();
@@ -163,7 +165,7 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<MareMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<MareMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<PairManager>(),
s.GetRequiredService<ILogger<GameChatHooks>>(), gameInteropProvider, chatGui, s.GetRequiredService<ILogger<GameChatHooks>>(), gameInteropProvider, chatGui,
s.GetRequiredService<MareConfigService>(), s.GetRequiredService<ServerConfigurationManager>())); s.GetRequiredService<MareConfigService>(), s.GetRequiredService<ServerConfigurationManager>()));
collection.AddScoped((s) => new GuiHookService(s.GetRequiredService<ILogger<GuiHookService>>(), s.GetRequiredService<MareMediator>(), collection.AddScoped((s) => new GuiHookService(s.GetRequiredService<ILogger<GuiHookService>>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<MareMediator>(),
s.GetRequiredService<MareConfigService>(), namePlateGui, s.GetRequiredService<PairManager>())); s.GetRequiredService<MareConfigService>(), namePlateGui, s.GetRequiredService<PairManager>()));
collection.AddHostedService(p => p.GetRequiredService<MareMediator>()); collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
@@ -178,7 +180,16 @@ public sealed class Plugin : IDalamudPlugin
}) })
.Build(); .Build();
_ = _host.StartAsync(); Task.Run(async () => {
try
{
await _host.StartAsync();
}
catch (Exception e)
{
pluginLog.Error(e, "HostBuilder startup exception");
}
});
} }
public void Dispose() public void Dispose()

View File

@@ -14,17 +14,19 @@ namespace MareSynchronos.Services;
public class GuiHookService : DisposableMediatorSubscriberBase public class GuiHookService : DisposableMediatorSubscriberBase
{ {
private readonly ILogger<GuiHookService> _logger; private readonly ILogger<GuiHookService> _logger;
private readonly DalamudUtilService _dalamudUtil;
private readonly MareConfigService _configService; private readonly MareConfigService _configService;
private readonly INamePlateGui _namePlateGui; private readonly INamePlateGui _namePlateGui;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private bool _isModified = false; private bool _isModified = false;
public GuiHookService(ILogger<GuiHookService> logger, MareMediator mediator, MareConfigService configService, public GuiHookService(ILogger<GuiHookService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, MareConfigService configService,
INamePlateGui namePlateGui, PairManager pairManager) INamePlateGui namePlateGui, PairManager pairManager)
: base(logger, mediator) : base(logger, mediator)
{ {
_logger = logger; _logger = logger;
_dalamudUtil = dalamudUtil;
_configService = configService; _configService = configService;
_namePlateGui = namePlateGui; _namePlateGui = namePlateGui;
_pairManager = pairManager; _pairManager = pairManager;
@@ -32,26 +34,32 @@ public class GuiHookService : DisposableMediatorSubscriberBase
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate; _namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
_namePlateGui.RequestRedraw(); _namePlateGui.RequestRedraw();
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => _namePlateGui.RequestRedraw()); Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw());
Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw());
} }
public void RequestRedraw() public void RequestRedraw(bool force = false)
{ {
if (!_configService.Current.UseNameColors) if (!_configService.Current.UseNameColors)
{ {
if (!_isModified) if (!_isModified && !force)
return; return;
_isModified = false; _isModified = false;
} }
_namePlateGui.RequestRedraw(); Task.Run(async () => {
await _dalamudUtil.RunOnFrameworkThread(() => _namePlateGui.RequestRedraw());
});
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate; _namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
_namePlateGui.RequestRedraw();
Task.Run(async () => {
await _dalamudUtil.RunOnFrameworkThread(() => _namePlateGui.RequestRedraw());
});
} }
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers) private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
@@ -59,13 +67,17 @@ public class GuiHookService : DisposableMediatorSubscriberBase
if (!_configService.Current.UseNameColors) if (!_configService.Current.UseNameColors)
return; return;
var visibleUsersIds = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue).Select(u => (ulong)u.PlayerCharacterId).ToHashSet(); var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue);
var colors = _configService.Current.NameColors; var visibleUsersIds = visibleUsers.Select(u => (ulong)u.PlayerCharacterId).ToHashSet();
var visibleUsersDict = visibleUsers.ToDictionary(u => (ulong)u.PlayerCharacterId);
foreach (var handler in handlers) foreach (var handler in handlers)
{ {
if (visibleUsersIds.Contains(handler.GameObjectId)) if (visibleUsersIds.Contains(handler.GameObjectId))
{ {
var pair = visibleUsersDict[handler.GameObjectId];
var colors = !pair.IsApplicationBlocked ? _configService.Current.NameColors : _configService.Current.BlockedNameColors;
handler.NameParts.TextWrap = ( handler.NameParts.TextWrap = (
BuildColorStartSeString(colors), BuildColorStartSeString(colors),
BuildColorEndSeString(colors) BuildColorEndSeString(colors)

View File

@@ -88,5 +88,11 @@ 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 RecalculatePerformanceMessage(string? UID) : MessageBase;
public record NameplateRedrawMessage : MessageBase;
public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
public record UnholdPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
public record HoldPairDownloadsMessage(string UID, string Source) : KeyedMessage(UID);
public record UnholdPairDownloadsMessage(string UID, string Source) : KeyedMessage(UID);
#pragma warning restore S2094 #pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name #pragma warning restore MA0048 // File name must match type name

View File

@@ -1,25 +1,40 @@
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
using MareSynchronos.FileCache; using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Events;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI;
using MareSynchronos.WebAPI.Files.Models; using MareSynchronos.WebAPI.Files.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services; namespace MareSynchronos.Services;
public class PlayerPerformanceService public class PlayerPerformanceService : DisposableMediatorSubscriberBase
{ {
// Limits that will still be enforced when no limits are enabled
public const int MaxVRAMUsageThreshold = 2000; // 2GB
public const int MaxTriUsageThreshold = 2000000; // 2 million triangles
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly XivDataAnalyzer _xivDataAnalyzer; private readonly XivDataAnalyzer _xivDataAnalyzer;
private readonly ILogger<PlayerPerformanceService> _logger; private readonly ILogger<PlayerPerformanceService> _logger;
private readonly MareMediator _mediator; private readonly MareMediator _mediator;
private readonly ServerConfigurationManager _serverConfigurationManager;
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,
FileCacheManager fileCacheManager, ServerConfigurationManager serverConfigurationManager,
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
XivDataAnalyzer xivDataAnalyzer) XivDataAnalyzer xivDataAnalyzer)
: base(logger, mediator)
{ {
_logger = logger; _logger = logger;
_mediator = mediator; _mediator = mediator;
_serverConfigurationManager = serverConfigurationManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_xivDataAnalyzer = xivDataAnalyzer; _xivDataAnalyzer = xivDataAnalyzer;
} }
@@ -36,6 +51,7 @@ public class PlayerPerformanceService
public async Task<bool> CheckTriangleUsageThresholds(PairHandler pairHandler, CharacterData charaData) public async Task<bool> CheckTriangleUsageThresholds(PairHandler pairHandler, CharacterData charaData)
{ {
var config = _playerPerformanceConfigService.Current;
var pair = pairHandler.Pair; var pair = pairHandler.Pair;
long triUsage = 0; long triUsage = 0;
@@ -58,13 +74,42 @@ public class PlayerPerformanceService
pair.LastAppliedDataTris = triUsage; pair.LastAppliedDataTris = triUsage;
_logger.LogDebug("Calculated VRAM usage for {p}", pairHandler); _logger.LogDebug("Calculated Triangle usage for {p}", pairHandler);
long triUsageThreshold = config.TrisAutoPauseThresholdThousands * 1000;
bool isDirect = pair.UserPair != null;
bool autoPause = config.AutoPausePlayersExceedingThresholds;
bool notify = isDirect ? config.NotifyAutoPauseDirectPairs : config.NotifyAutoPauseGroupPairs;
if (autoPause && isDirect && config.IgnoreDirectPairs)
autoPause = false;
if (!autoPause || _serverConfigurationManager.IsUidWhitelisted(pair.UserData.UID))
triUsageThreshold = MaxTriUsageThreshold;
if (triUsage > triUsageThreshold)
{
if (notify && !pair.IsApplicationBlocked)
{
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically blocked",
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto block threshold (" +
$"{triUsage}/{triUsageThreshold} triangles)" +
$" and has been automatically blocked.",
MareConfiguration.Models.NotificationType.Warning));
}
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
$"Exceeds triangle threshold: ({triUsage}/{triUsageThreshold} triangles)")));
return false;
}
return true; return true;
} }
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(PairHandler pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles) public bool ComputeAndAutoPauseOnVRAMUsageThresholds(PairHandler pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
{ {
var config = _playerPerformanceConfigService.Current;
var pair = pairHandler.Pair; var pair = pairHandler.Pair;
long vramUsage = 0; long vramUsage = 0;
@@ -110,6 +155,34 @@ public class PlayerPerformanceService
_logger.LogDebug("Calculated VRAM usage for {p}", pairHandler); _logger.LogDebug("Calculated VRAM usage for {p}", pairHandler);
long vramUsageThreshold = config.VRAMSizeAutoPauseThresholdMiB;
bool isDirect = pair.UserPair != null;
bool autoPause = config.AutoPausePlayersExceedingThresholds;
bool notify = isDirect ? config.NotifyAutoPauseDirectPairs : config.NotifyAutoPauseGroupPairs;
if (autoPause && isDirect && config.IgnoreDirectPairs)
autoPause = false;
if (!autoPause || _serverConfigurationManager.IsUidWhitelisted(pair.UserData.UID))
vramUsageThreshold = MaxVRAMUsageThreshold;
if (vramUsage > vramUsageThreshold * 1024 * 1024)
{
if (notify && !pair.IsApplicationBlocked)
{
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically blocked",
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto block threshold (" +
$"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{vramUsageThreshold}MiB)" +
$" and has been automatically blocked.",
MareConfiguration.Models.NotificationType.Warning));
}
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
$"Exceeds VRAM threshold: ({UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{vramUsageThreshold} MiB)")));
return false;
}
return true; return true;
} }
} }

View File

@@ -15,11 +15,16 @@ public class ServerConfigurationManager
private readonly ILogger<ServerConfigurationManager> _logger; private readonly ILogger<ServerConfigurationManager> _logger;
private readonly MareMediator _mareMediator; private readonly MareMediator _mareMediator;
private readonly NotesConfigService _notesConfig; private readonly NotesConfigService _notesConfig;
private readonly ServerBlockConfigService _blockConfig;
private readonly ServerTagConfigService _serverTagConfig; private readonly ServerTagConfigService _serverTagConfig;
private readonly SyncshellConfigService _syncshellConfig; private readonly SyncshellConfigService _syncshellConfig;
private HashSet<string>? CachedWhitelistedUIDs = null;
private HashSet<string>? CachedBlacklistedUIDs = null;
public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService, public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
ServerTagConfigService serverTagConfig, SyncshellConfigService syncshellConfig, NotesConfigService notesConfig, ServerTagConfigService serverTagConfig, SyncshellConfigService syncshellConfig, NotesConfigService notesConfig,
ServerBlockConfigService blockConfig,
DalamudUtilService dalamudUtil, MareMediator mareMediator) DalamudUtilService dalamudUtil, MareMediator mareMediator)
{ {
_logger = logger; _logger = logger;
@@ -27,6 +32,7 @@ public class ServerConfigurationManager
_serverTagConfig = serverTagConfig; _serverTagConfig = serverTagConfig;
_syncshellConfig = syncshellConfig; _syncshellConfig = syncshellConfig;
_notesConfig = notesConfig; _notesConfig = notesConfig;
_blockConfig = blockConfig;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_mareMediator = mareMediator; _mareMediator = mareMediator;
EnsureMainExists(); EnsureMainExists();
@@ -35,11 +41,16 @@ public class ServerConfigurationManager
public string CurrentApiUrl => CurrentServer.ServerUri; public string CurrentApiUrl => CurrentServer.ServerUri;
public ServerStorage CurrentServer => _configService.Current.ServerStorage[CurrentServerIndex]; public ServerStorage CurrentServer => _configService.Current.ServerStorage[CurrentServerIndex];
public IReadOnlyList<string> Whitelist => CurrentBlockStorage().Whitelist;
public IReadOnlyList<string> Blacklist => CurrentBlockStorage().Blacklist;
public int CurrentServerIndex public int CurrentServerIndex
{ {
set set
{ {
_configService.Current.CurrentServer = value; _configService.Current.CurrentServer = value;
CachedWhitelistedUIDs = null;
CachedBlacklistedUIDs = null;
_configService.Save(); _configService.Save();
} }
get get
@@ -403,6 +414,54 @@ public class ServerConfigurationManager
_syncshellConfig.Save(); _syncshellConfig.Save();
} }
internal bool IsUidWhitelisted(string uid)
{
CachedWhitelistedUIDs ??= [.. CurrentBlockStorage().Whitelist];
return CachedWhitelistedUIDs.Contains(uid);
}
internal bool IsUidBlacklisted(string uid)
{
CachedBlacklistedUIDs ??= [.. CurrentBlockStorage().Blacklist];
return CachedBlacklistedUIDs.Contains(uid);
}
internal void AddWhitelistUid(string uid)
{
if (IsUidWhitelisted(uid))
return;
if (CurrentBlockStorage().Blacklist.RemoveAll(u => u == uid) > 0)
CachedBlacklistedUIDs = null;
CurrentBlockStorage().Whitelist.Add(uid);
CachedWhitelistedUIDs = null;
_blockConfig.Save();
}
internal void AddBlacklistUid(string uid)
{
if (IsUidBlacklisted(uid))
return;
if (CurrentBlockStorage().Whitelist.RemoveAll(u => u == uid) > 0)
CachedWhitelistedUIDs = null;
CurrentBlockStorage().Blacklist.Add(uid);
CachedBlacklistedUIDs = null;
_blockConfig.Save();
}
internal void RemoveWhitelistUid(string uid)
{
if (CurrentBlockStorage().Whitelist.RemoveAll(u => u == uid) > 0)
CachedWhitelistedUIDs = null;
_blockConfig.Save();
}
internal void RemoveBlacklistUid(string uid)
{
if (CurrentBlockStorage().Blacklist.RemoveAll(u => u == uid) > 0)
CachedBlacklistedUIDs = null;
_blockConfig.Save();
}
private ServerNotesStorage CurrentNotesStorage() private ServerNotesStorage CurrentNotesStorage()
{ {
TryCreateCurrentNotesStorage(); TryCreateCurrentNotesStorage();
@@ -421,6 +480,12 @@ public class ServerConfigurationManager
return _syncshellConfig.Current.ServerShellStorage[CurrentApiUrl]; return _syncshellConfig.Current.ServerShellStorage[CurrentApiUrl];
} }
private ServerBlockStorage CurrentBlockStorage()
{
TryCreateCurrentBlockStorage();
return _blockConfig.Current.ServerBlocks[CurrentApiUrl];
}
private void EnsureMainExists() private void EnsureMainExists()
{ {
bool lopExists = false; bool lopExists = false;
@@ -478,4 +543,12 @@ public class ServerConfigurationManager
_syncshellConfig.Current.ServerShellStorage[CurrentApiUrl] = new(); _syncshellConfig.Current.ServerShellStorage[CurrentApiUrl] = new();
} }
} }
private void TryCreateCurrentBlockStorage()
{
if (!_blockConfig.Current.ServerBlocks.ContainsKey(CurrentApiUrl))
{
_blockConfig.Current.ServerBlocks[CurrentApiUrl] = new();
}
}
} }

View File

@@ -52,6 +52,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly GuiHookService _guiHookService; private readonly GuiHookService _guiHookService;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly PlayerPerformanceService _playerPerformanceService;
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
private bool _deleteAccountPopupModalShown = false; private bool _deleteAccountPopupModalShown = false;
private bool _deleteFilesPopupModalShown = false; private bool _deleteFilesPopupModalShown = false;
@@ -76,6 +78,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
UiSharedService uiShared, MareConfigService configService, UiSharedService uiShared, MareConfigService configService,
MareCharaFileManager mareCharaFileManager, PairManager pairManager, ChatService chatService, GuiHookService guiHookService, MareCharaFileManager mareCharaFileManager, PairManager pairManager, ChatService chatService, GuiHookService guiHookService,
ServerConfigurationManager serverConfigurationManager, ServerConfigurationManager serverConfigurationManager,
PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceService playerPerformanceService,
MareMediator mediator, PerformanceCollectorService performanceCollector, MareMediator mediator, PerformanceCollectorService performanceCollector,
FileUploadManager fileTransferManager, FileUploadManager fileTransferManager,
FileTransferOrchestrator fileTransferOrchestrator, FileTransferOrchestrator fileTransferOrchestrator,
@@ -90,6 +93,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
_chatService = chatService; _chatService = chatService;
_guiHookService = guiHookService; _guiHookService = guiHookService;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
_playerPerformanceService = playerPerformanceService;
_performanceCollector = performanceCollector; _performanceCollector = performanceCollector;
_fileTransferManager = fileTransferManager; _fileTransferManager = fileTransferManager;
_fileTransferOrchestrator = fileTransferOrchestrator; _fileTransferOrchestrator = fileTransferOrchestrator;
@@ -673,6 +678,18 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Current.LogEvents = logEvents; _configService.Current.LogEvents = logEvents;
_configService.Save(); _configService.Save();
} }
ImGui.Separator();
_uiShared.BigText("Active Character Blocks");
foreach (var pair in _pairManager.GetOnlineUserPairs())
{
if (pair.IsApplicationBlocked)
{
ImGui.TextUnformatted(pair.PlayerName);
ImGui.SameLine();
ImGui.TextUnformatted(string.Join(", ", pair.HoldApplicationReasons));
}
}
} }
private void DrawFileStorageSettings() private void DrawFileStorageSettings()
@@ -1055,6 +1072,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
var useNameColors = _configService.Current.UseNameColors; var useNameColors = _configService.Current.UseNameColors;
var nameColors = _configService.Current.NameColors; var nameColors = _configService.Current.NameColors;
var autoPausedNameColors = _configService.Current.BlockedNameColors;
if (ImGui.Checkbox("Color nameplates of paired players", ref useNameColors)) if (ImGui.Checkbox("Color nameplates of paired players", ref useNameColors))
{ {
_configService.Current.UseNameColors = useNameColors; _configService.Current.UseNameColors = useNameColors;
@@ -1071,6 +1089,15 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Save(); _configService.Save();
_guiHookService.RequestRedraw(); _guiHookService.RequestRedraw();
} }
ImGui.SameLine();
if (InputDtrColors("Blocked Character Color", ref autoPausedNameColors))
{
_configService.Current.BlockedNameColors = autoPausedNameColors;
_configService.Save();
_guiHookService.RequestRedraw();
}
} }
if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate))
@@ -1205,6 +1232,215 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
} }
private bool _perfUnapplied = false;
private void DrawPerformance()
{
_uiShared.BigText("Performance Settings");
UiSharedService.TextWrapped("The configuration options here are to give you more informed warnings and automation when it comes to other performance-intensive synced players.");
ImGui.Separator();
bool recalculatePerformance = false;
string? recalculatePerformanceUID = null;
_uiShared.BigText("Individual Limits");
bool autoPause = _playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds;
if (ImGui.Checkbox("Automatically block players exceeding thresholds", ref autoPause))
{
_playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds = autoPause;
_playerPerformanceConfigService.Save();
recalculatePerformance = true;
}
_uiShared.DrawHelpText("When enabled, it will automatically block the modded appearance of all players that exceed the thresholds defined below." + Environment.NewLine
+ "Will print a warning in chat when a player is blocked automatically.");
using (ImRaii.Disabled(!autoPause))
{
using var indent = ImRaii.PushIndent();
var notifyDirectPairs = _playerPerformanceConfigService.Current.NotifyAutoPauseDirectPairs;
var notifyGroupPairs = _playerPerformanceConfigService.Current.NotifyAutoPauseGroupPairs;
if (ImGui.Checkbox("Display auto-block warnings for individual pairs", ref notifyDirectPairs))
{
_playerPerformanceConfigService.Current.NotifyAutoPauseDirectPairs = notifyDirectPairs;
_playerPerformanceConfigService.Save();
}
if (ImGui.Checkbox("Display auto-block warnings for syncshell pairs", ref notifyGroupPairs))
{
_playerPerformanceConfigService.Current.NotifyAutoPauseGroupPairs = notifyGroupPairs;
_playerPerformanceConfigService.Save();
}
var vramAuto = _playerPerformanceConfigService.Current.VRAMSizeAutoPauseThresholdMiB;
var trisAuto = _playerPerformanceConfigService.Current.TrisAutoPauseThresholdThousands;
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("Auto Block VRAM threshold", ref vramAuto))
{
_playerPerformanceConfigService.Current.VRAMSizeAutoPauseThresholdMiB = vramAuto;
_playerPerformanceConfigService.Save();
_perfUnapplied = true;
}
ImGui.SameLine();
ImGui.Text("(MiB)");
_uiShared.DrawHelpText("When a loading in player and their VRAM usage exceeds this amount, automatically blocks the synced player." + UiSharedService.TooltipSeparator
+ "Default: 550 MiB");
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("Auto Block Triangle threshold", ref trisAuto))
{
_playerPerformanceConfigService.Current.TrisAutoPauseThresholdThousands = trisAuto;
_playerPerformanceConfigService.Save();
_perfUnapplied = true;
}
ImGui.SameLine();
ImGui.Text("(thousand triangles)");
_uiShared.DrawHelpText("When a loading in player and their triangle count exceeds this amount, automatically blocks the synced player." + UiSharedService.TooltipSeparator
+ "Default: 250 thousand");
using (ImRaii.Disabled(!_perfUnapplied))
{
if (ImGui.Button("Apply Changes Now"))
{
recalculatePerformance = true;
_perfUnapplied = false;
}
}
}
#region Whitelist
ImGui.Separator();
_uiShared.BigText("Whitelisted UIDs");
bool ignoreDirectPairs = _playerPerformanceConfigService.Current.IgnoreDirectPairs;
if (ImGui.Checkbox("Whitelist all individual pairs", ref ignoreDirectPairs))
{
_playerPerformanceConfigService.Current.IgnoreDirectPairs = ignoreDirectPairs;
_playerPerformanceConfigService.Save();
recalculatePerformance = true;
}
_uiShared.DrawHelpText("Individual pairs will never be affected by auto blocks.");
ImGui.Dummy(new Vector2(5));
UiSharedService.TextWrapped("The entries in the list below will be not have auto block thresholds enforced.");
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
var whitelistPos = ImGui.GetCursorPos();
ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale);
ImGui.InputText("##whitelistuid", ref _uidToAddForIgnore, 20);
using (ImRaii.Disabled(string.IsNullOrEmpty(_uidToAddForIgnore)))
{
ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale);
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add UID to whitelist"))
{
if (!_serverConfigurationManager.IsUidWhitelisted(_uidToAddForIgnore))
{
_serverConfigurationManager.AddWhitelistUid(_uidToAddForIgnore);
recalculatePerformance = true;
recalculatePerformanceUID = _uidToAddForIgnore;
}
_uidToAddForIgnore = string.Empty;
}
}
ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale);
_uiShared.DrawHelpText("Hint: UIDs are case sensitive.\nVanity IDs are also acceptable.");
ImGui.Dummy(new Vector2(10));
var playerList = _serverConfigurationManager.Whitelist;
if (_selectedEntry > playerList.Count - 1)
_selectedEntry = -1;
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
ImGui.SetCursorPosY(whitelistPos.Y);
using (var lb = ImRaii.ListBox("##whitelist"))
{
if (lb)
{
for (int i = 0; i < playerList.Count; i++)
{
bool shouldBeSelected = _selectedEntry == i;
if (ImGui.Selectable(playerList[i] + "##" + i, shouldBeSelected))
{
_selectedEntry = i;
}
string? lastSeenName = _serverConfigurationManager.GetNameForUid(playerList[i]);
if (lastSeenName != null)
{
ImGui.SameLine();
_uiShared.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip($"Last seen name: {lastSeenName}");
}
}
}
}
using (ImRaii.Disabled(_selectedEntry == -1))
{
using var pushId = ImRaii.PushId("deleteSelectedWhitelist");
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete selected UID"))
{
_serverConfigurationManager.RemoveWhitelistUid(_serverConfigurationManager.Whitelist[_selectedEntry]);
_selectedEntry = -1;
_playerPerformanceConfigService.Save();
recalculatePerformance = true;
}
}
#endregion Whitelist
#region Blacklist
ImGui.Separator();
_uiShared.BigText("Blacklisted UIDs");
UiSharedService.TextWrapped("The entries in the list below will never have their characters displayed.");
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
var blacklistPos = ImGui.GetCursorPos();
ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale);
ImGui.InputText("##uid", ref _uidToAddForIgnoreBlacklist, 20);
using (ImRaii.Disabled(string.IsNullOrEmpty(_uidToAddForIgnoreBlacklist)))
{
ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale);
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add UID to blacklist"))
{
if (!_serverConfigurationManager.IsUidBlacklisted(_uidToAddForIgnoreBlacklist))
{
_serverConfigurationManager.AddBlacklistUid(_uidToAddForIgnoreBlacklist);
recalculatePerformance = true;
recalculatePerformanceUID = _uidToAddForIgnoreBlacklist;
}
_uidToAddForIgnoreBlacklist = string.Empty;
}
}
_uiShared.DrawHelpText("Hint: UIDs are case sensitive.\nVanity IDs are also acceptable.");
ImGui.Dummy(new Vector2(10));
var blacklist = _serverConfigurationManager.Blacklist;
if (_selectedEntryBlacklist > blacklist.Count - 1)
_selectedEntryBlacklist = -1;
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
ImGui.SetCursorPosY(blacklistPos.Y);
using (var lb = ImRaii.ListBox("##blacklist"))
{
if (lb)
{
for (int i = 0; i < blacklist.Count; i++)
{
bool shouldBeSelected = _selectedEntryBlacklist == i;
if (ImGui.Selectable(blacklist[i] + "##BL" + i, shouldBeSelected))
{
_selectedEntryBlacklist = i;
}
string? lastSeenName = _serverConfigurationManager.GetNameForUid(blacklist[i]);
if (lastSeenName != null)
{
ImGui.SameLine();
_uiShared.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip($"Last seen name: {lastSeenName}");
}
}
}
}
using (ImRaii.Disabled(_selectedEntryBlacklist == -1))
{
using var pushId = ImRaii.PushId("deleteSelectedBlacklist");
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete selected UID"))
{
_serverConfigurationManager.RemoveBlacklistUid(_serverConfigurationManager.Blacklist[_selectedEntryBlacklist]);
_selectedEntryBlacklist = -1;
_playerPerformanceConfigService.Save();
recalculatePerformance = true;
}
}
#endregion Blacklist
if (recalculatePerformance)
Mediator.Publish(new RecalculatePerformanceMessage(recalculatePerformanceUID));
}
private static bool InputDtrColors(string label, ref DtrEntry.Colors colors) private static bool InputDtrColors(string label, ref DtrEntry.Colors colors)
{ {
using var id = ImRaii.PushId(label); using var id = ImRaii.PushId(label);
@@ -1580,6 +1816,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
} }
private string _uidToAddForIgnore = string.Empty;
private int _selectedEntry = -1;
private string _uidToAddForIgnoreBlacklist = string.Empty;
private int _selectedEntryBlacklist = -1;
private void DrawSettingsContent() private void DrawSettingsContent()
{ {
if (_apiController.ServerState is ServerState.Connected) if (_apiController.ServerState is ServerState.Connected)
@@ -1605,6 +1847,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
if (ImGui.BeginTabItem("Performance"))
{
DrawPerformance();
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Export & Storage")) if (ImGui.BeginTabItem("Export & Storage"))
{ {
DrawFileStorageSettings(); DrawFileStorageSettings();