using MareSynchronos.API.Data; using MareSynchronos.FileCache; using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.Services.Events; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.UI; using MareSynchronos.WebAPI.Files.Models; using Microsoft.Extensions.Logging; namespace MareSynchronos.Services; 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 XivDataAnalyzer _xivDataAnalyzer; private readonly ILogger _logger; private readonly MareMediator _mediator; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly Dictionary _warnedForPlayers = new(StringComparer.Ordinal); public PlayerPerformanceService(ILogger logger, MareMediator mediator, ServerConfigurationManager serverConfigurationManager, PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager, XivDataAnalyzer xivDataAnalyzer) : base(logger, mediator) { _logger = logger; _mediator = mediator; _serverConfigurationManager = serverConfigurationManager; _playerPerformanceConfigService = playerPerformanceConfigService; _fileCacheManager = fileCacheManager; _xivDataAnalyzer = xivDataAnalyzer; } public async Task CheckBothThresholds(PairHandler pairHandler, CharacterData charaData) { bool notPausedAfterVram = ComputeAndAutoPauseOnVRAMUsageThresholds(pairHandler, charaData, []); if (!notPausedAfterVram) return false; bool notPausedAfterTris = await CheckTriangleUsageThresholds(pairHandler, charaData).ConfigureAwait(false); if (!notPausedAfterTris) return false; return true; } public async Task CheckTriangleUsageThresholds(PairHandler pairHandler, CharacterData charaData) { var config = _playerPerformanceConfigService.Current; var pair = pairHandler.Pair; long triUsage = 0; if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List? playerReplacements)) { pair.LastAppliedDataTris = 0; return true; } var moddedModelHashes = playerReplacements.Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase))) .Select(p => p.Hash) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); foreach (var hash in moddedModelHashes) { triUsage += await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false); } pair.LastAppliedDataTris = triUsage; _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; } public bool ComputeAndAutoPauseOnVRAMUsageThresholds(PairHandler pairHandler, CharacterData charaData, List toDownloadFiles) { var config = _playerPerformanceConfigService.Current; var pair = pairHandler.Pair; long vramUsage = 0; if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List? playerReplacements)) { pair.LastAppliedApproximateVRAMBytes = 0; return true; } var moddedTextureHashes = playerReplacements.Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))) .Select(p => p.Hash) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); foreach (var hash in moddedTextureHashes) { long fileSize = 0; var download = toDownloadFiles.Find(f => string.Equals(hash, f.Hash, StringComparison.OrdinalIgnoreCase)); if (download != null) { fileSize = download.TotalRaw; } else { var fileEntry = _fileCacheManager.GetFileCacheByHash(hash); if (fileEntry == null) continue; if (fileEntry.Size == null) { fileEntry.Size = new FileInfo(fileEntry.ResolvedFilepath).Length; _fileCacheManager.UpdateHashedFile(fileEntry, computeProperties: true); } fileSize = fileEntry.Size.Value; } vramUsage += fileSize; } pair.LastAppliedApproximateVRAMBytes = vramUsage; _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; } }