diff --git a/README.md b/README.md index 372ed5f..092443a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,13 @@ Click the windows application or open a Powershell or Command Line prompt in you Or if you have moved where the VR Chat ```output_log_*.txt``` are located; then: -```.\tailgrab.exe {full path to VR Chat logs ending with a \}``` +```.\tailgrab.exe -l {full path to VR Chat logs ending with a \}``` + +If you need to clear all registry settings stored for TailGrab, you can run: + +```.\tailgrab.exe -clear``` + +This will remove all stored configuration and secret values from the Windows Registry for TailGrab, you can then reconfigure the application as needed, save them, restart and get back to watching the instance. ## VRChat Source Log Files diff --git a/src/Actions/Actions.cs b/src/Actions/Actions.cs index 7573980..ff62d5f 100644 --- a/src/Actions/Actions.cs +++ b/src/Actions/Actions.cs @@ -11,7 +11,7 @@ namespace Tailgrab.Actions public interface IAction { void PerformAction(); - } + } public class DelayAction : IAction @@ -22,13 +22,13 @@ public class DelayAction : IAction public DelayAction(int delayMilliseconds) { DelayMilliseconds = delayMilliseconds; - logger.Warn($"Added DelayAction: Will delay for : '{DelayMilliseconds}' milliseconds."); + logger.Warn($"Added DelayAction: Will delay for : '{DelayMilliseconds}' milliseconds."); } public void PerformAction() { - if( DelayMilliseconds <= 0 ) + if (DelayMilliseconds <= 0) { return; } @@ -50,14 +50,14 @@ public KeystrokesAction(string windowTitle, string keys) WindowTitle = windowTitle; Keys = keys; - logger.Warn($"Added KeystrokesAction: Window Title: '{WindowTitle}' with Keys: {Keys}."); + logger.Warn($"Added KeystrokesAction: Window Title: '{WindowTitle}' with Keys: {Keys}."); } public void PerformAction() { - if( WindowTitle == null || Keys == null ) + if (WindowTitle == null || Keys == null) { - logger.Warn($"KeystrokesAction: Window Title: '{WindowTitle}' or Keys: {Keys} not supplied."); + logger.Warn($"KeystrokesAction: Window Title: '{WindowTitle}' or Keys: {Keys} not supplied."); return; } @@ -252,9 +252,9 @@ public OSCAction(string parameterName, OscType type, string value) OscTypeValue = type; Value = value; - logger.Warn($"Added OSCAction: Parameter: '{ParameterName}'; Type: {OscTypeValue}; Value: {Value}."); + logger.Warn($"Added OSCAction: Parameter: '{ParameterName}'; Type: {OscTypeValue}; Value: {Value}."); - } + } public void PerformAction() { @@ -269,7 +269,7 @@ public void PerformAction() { case OscType.Bool: if (bool.TryParse(value, out bool boolValue)) - { + { OscParameter.SendValue(parameterName, boolValue); } break; @@ -285,7 +285,7 @@ public void PerformAction() OscParameter.SendValue(parameterName, floatValue); } break; - } + } } } @@ -293,7 +293,7 @@ public class TTSAction : IAction { public Logger logger = LogManager.GetCurrentClassLogger(); - public int Volume{ get; set; } = 100; + public int Volume { get; set; } = 100; public int Rate { get; set; } = 0; diff --git a/src/AvatarManagement/AvatarManagement.cs b/src/AvatarManagement/AvatarManagement.cs index c2af37d..bea9128 100644 --- a/src/AvatarManagement/AvatarManagement.cs +++ b/src/AvatarManagement/AvatarManagement.cs @@ -1,9 +1,11 @@ -using Microsoft.EntityFrameworkCore; +using ConcurrentPriorityQueue.Core; +using Microsoft.EntityFrameworkCore; using NLog; using System.Media; -using Tailgrab; +using Tailgrab.Clients.Ollama; +using Tailgrab.Common; +using Tailgrab.Config; using Tailgrab.Models; -using Tailgrab.Clients.VRChat; using VRChat.API.Model; namespace Tailgrab.AvatarManagement @@ -14,11 +16,15 @@ public class AvatarManagementService private ServiceRegistry _serviceRegistry; - private static List avatarsInSession = new List(); + private ConcurrentPriorityQueue, int> priorityQueue = new ConcurrentPriorityQueue, int>(); + public AvatarManagementService(ServiceRegistry serviceRegistry) { - _serviceRegistry = serviceRegistry; + _serviceRegistry = serviceRegistry; + + _ = Task.Run(() => AvatarCheckTask(priorityQueue, _serviceRegistry)); + } public void AddAvatar(AvatarInfo avatar) @@ -69,7 +75,8 @@ public void CacheAvatars(List avatarIdInCache) int postion = 0; foreach (var avatarId in avatarIdInCache) - { + { + EnqueueAvatarForCheck(avatarId); AvatarInfo? dbAvatarInfo = dbContext.AvatarInfos.Find(avatarId); bool updateNeeded = false; if (dbAvatarInfo == null) @@ -121,7 +128,7 @@ public void CacheAvatars(List avatarIdInCache) var entry = dbContext.Entry(dbAvatarInfo); if (entry.State == Microsoft.EntityFrameworkCore.EntityState.Detached) { - dbContext.Attach(dbAvatarInfo); + dbContext.Attach(dbAvatarInfo); entry = _serviceRegistry.GetDBContext().Entry(dbAvatarInfo); } @@ -147,8 +154,8 @@ public void CacheAvatars(List avatarIdInCache) { logger.Error($"Error fetching avatar: {ex.Message}"); } - - if (avatarData == null && dbAvatarInfo == null ) + + if (avatarData == null && dbAvatarInfo == null) { var avatarInfo = new AvatarInfo { @@ -176,11 +183,20 @@ public void CacheAvatars(List avatarIdInCache) postion++; } + } - avatarsInSession.Clear(); + private void EnqueueAvatarForCheck(string avatarId) + { + var queuedItem = new QueuedAvatarProcess + { + AvatarId = avatarId, + Priority = 1 + }; + + priorityQueue.Enqueue(queuedItem); } - public void GetAvatarsFromUser( string userId, string avatarName ) + public void GetAvatarsFromUser(string userId, string avatarName) { logger.Debug($"Fetching avatars for user {userId} to find avatar named {avatarName}"); @@ -243,19 +259,170 @@ internal bool CheckAvatarByName(string avatarName) if (bannedAvatars.Count > 0) { - SystemSounds.Hand.Play(); + string? soundSetting = ConfigStore.LoadSecret(Common.Common.Registry_Alert_Avatar) ?? "Hand"; + SoundManager.PlaySound(soundSetting); return true; } return false; } - internal void AddAvatarsInSession(string avatarName) + public static async Task AvatarCheckTask(ConcurrentPriorityQueue, int> priorityQueue, ServiceRegistry serviceRegistry) + { + OllamaClient.logger.Info($"Amplitude Avatar Cache Queue Running"); + TailgrabDBContext dBContext = serviceRegistry.GetDBContext(); + while (true) + { + // Process items from the priority queue + while (true) + { + var result = priorityQueue.Dequeue(); + if (result.IsSuccess && result.Value is QueuedAvatarProcess item && item.AvatarId != null) + { + try + { + AvatarInfo? dbAvatarInfo = dBContext.AvatarInfos.Find(item.AvatarId); + bool updateNeeded = false; + if (dbAvatarInfo == null) + { + updateNeeded = true; + } + else if (!dbAvatarInfo.IsBos && + (!dbAvatarInfo.UpdatedAt.HasValue || dbAvatarInfo.UpdatedAt.Value >= DateTime.UtcNow.AddHours(-24))) + { + updateNeeded = true; + } + + if (updateNeeded) + { + Avatar? avatarData = FetchUpdateAvatarData(serviceRegistry, dBContext, item.AvatarId, dbAvatarInfo); + + if (avatarData == null && dbAvatarInfo == null) + { + CreateAvatarInfoForPrivate(dBContext, item.AvatarId); + } + + // Wait for a short period before checking the queue again + await Task.Delay(1000); + } + } + catch (Exception ex) + { + logger.Error(ex, $"Error fetching user profile for userId: {item.AvatarId}"); + } + } + else + { + // No more items to process + break; + } + } + // Wait for a short period before checking the queue again + await Task.Delay(5000); + } + } + + private static void CreateAvatarInfoForPrivate(TailgrabDBContext dBContext, string AvatarId) + { + var avatarInfo = new AvatarInfo + { + AvatarId = AvatarId, + UserId = "", + AvatarName = $"Unknown Avatar {AvatarId}", + ImageUrl = "", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + IsBos = false + }; + + try + { + dBContext.Add(avatarInfo); + dBContext.SaveChanges(); + logger.Debug($"Adding fallback avatar record for {avatarInfo.ToString()}"); + } + catch (Exception ex) + { + logger.Error($"Error adding fallback avatar record for {AvatarId}: {ex.Message}"); + } + } + + private static Avatar? FetchUpdateAvatarData(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, string AvatarId, AvatarInfo? dbAvatarInfo) { - if (!avatarsInSession.Contains(avatarName)) + Avatar? avatarData = null; + try { - avatarsInSession.Add(avatarName); + // Avatar already exists in the database and was updated within the last 12 hours + System.Threading.Thread.Sleep(500); + avatarData = serviceRegistry.GetVRChatAPIClient().GetAvatarById(AvatarId); + if (avatarData != null) + { + if (dbAvatarInfo == null) + { + var avatarInfo = new AvatarInfo + { + AvatarId = avatarData.Id, + UserId = avatarData.AuthorId, + AvatarName = avatarData.Name, + ImageUrl = avatarData.ImageUrl, + CreatedAt = avatarData.CreatedAt, + UpdatedAt = DateTime.UtcNow, + IsBos = false + }; + + try + { + dBContext.Add(avatarInfo); + dBContext.SaveChanges(); + } + catch (Exception ex) + { + logger.Error($"Error adding avatar record for {AvatarId}: {ex.Message}"); + } + } + else + { + // Ensure entity is attached to the dbContext before updating to avoid Detached state errors + var entry = dBContext.Entry(dbAvatarInfo); + if (entry.State == Microsoft.EntityFrameworkCore.EntityState.Detached) + { + dBContext.Attach(dbAvatarInfo); + entry = dBContext.Entry(dbAvatarInfo); + } + + dbAvatarInfo.UserId = avatarData.AuthorId; + dbAvatarInfo.AvatarName = avatarData.Name; + dbAvatarInfo.ImageUrl = avatarData.ImageUrl; + dbAvatarInfo.CreatedAt = avatarData.CreatedAt; + dbAvatarInfo.UpdatedAt = DateTime.UtcNow; + + try + { + entry.State = Microsoft.EntityFrameworkCore.EntityState.Modified; + dBContext.SaveChanges(); + } + catch (Exception ex) + { + logger.Error($"Error updating avatar record for {AvatarId}: {ex.Message}"); + } + } + } } + catch (Exception ex) + { + logger.Error($"Error fetching avatar: {ex.Message}"); + } + + return avatarData; } } + + internal class QueuedAvatarProcess : IHavePriority + { + public int Priority { get; set; } + + public string? AvatarId { get; set; } + } + + } diff --git a/src/Clients/Ollama/Ollama.cs b/src/Clients/Ollama/Ollama.cs index a243622..a713ccb 100644 --- a/src/Clients/Ollama/Ollama.cs +++ b/src/Clients/Ollama/Ollama.cs @@ -3,6 +3,7 @@ using NLog; using OllamaSharp; using OllamaSharp.Models; +using System.Media; using System.Net.Http; using System.Text.RegularExpressions; using Tailgrab.Common; @@ -10,7 +11,6 @@ using Tailgrab.Models; using Tailgrab.PlayerManagement; using VRChat.API.Model; -using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel; namespace Tailgrab.Clients.Ollama { @@ -24,17 +24,17 @@ internal class QueuedProcess : IHavePriority public string? UserId { get; set; } public string? UserBio { get; set; } - public string MD5Hash - { + public string MD5Hash + { get { - if( string.IsNullOrEmpty(UserBio)) + if (string.IsNullOrEmpty(UserBio)) { return string.Empty; } // Remove all whitespace for hashing - return Checksum.CreateMD5(sWhitespace.Replace(UserBio,"")); + return Checksum.CreateMD5(sWhitespace.Replace(UserBio, "")); } } } @@ -48,7 +48,7 @@ public class OllamaClient public OllamaClient(ServiceRegistry registry) { - if(registry == null) + if (registry == null) { throw new ArgumentNullException(nameof(registry)); } @@ -63,14 +63,14 @@ public void CheckUserProfile(string userId) logger.Debug($"Checking user profile with AI : {userId}"); try - { + { QueuedProcess process = new QueuedProcess { UserId = userId, Priority = 1 }; - priorityQueue.Enqueue(process); + priorityQueue.Enqueue(process); } catch (Exception ex) { @@ -78,25 +78,26 @@ public void CheckUserProfile(string userId) } } - public static async Task ProfileCheckTask(ConcurrentPriorityQueue, int> priorityQueue, Dictionary processData, ServiceRegistry serviceRegistry ) + public static async Task ProfileCheckTask(ConcurrentPriorityQueue, int> priorityQueue, Dictionary processData, ServiceRegistry serviceRegistry) { string? ollamaCloudKey = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Key); - + OllamaApiClient? ollamaApi = null; if (ollamaCloudKey is null) { - System.Windows.MessageBox.Show("Ollama API Credentials are not set yet, use the Config / Secrets tab to update credenials and restart Tailgrab.", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); - return; + System.Windows.MessageBox.Show("Ollama API Credentials are not set.\nThis is not nessasary for limited operation, the Profiles will not be evaluated.\nOtherwise use the Config / Secrets tab to update credenials and restart Tailgrab.", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + } + else + { + string ollamaEndpoint = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Endpoint) ?? Tailgrab.Common.Common.Default_Ollama_API_Endpoint; + HttpClient client = new HttpClient(); + client.BaseAddress = new Uri(ollamaEndpoint); + client.DefaultRequestHeaders.Add("Authorization", "Bearer " + ollamaCloudKey); + ollamaApi = new OllamaApiClient(client); + string? ollamaModel = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Model) ?? Tailgrab.Common.Common.Default_Ollama_API_Model; + ollamaApi.SelectedModel = ollamaModel; } - string ollamaEndpoint = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Endpoint) ?? Tailgrab.Common.Common.Default_Ollama_API_Endpoint; - HttpClient client = new HttpClient(); - client.BaseAddress = new Uri(ollamaEndpoint); - client.DefaultRequestHeaders.Add("Authorization", "Bearer " + ollamaCloudKey); - OllamaApiClient? ollamaApi = new OllamaApiClient(client); - string ollamaModel = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Model) ?? Tailgrab.Common.Common.Default_Ollama_API_Model; - ollamaApi.SelectedModel = ollamaModel; - - OllamaClient.logger.Info($"OLlama Queue Running"); + OllamaClient.logger.Info($"Profile/Group Queue Running"); while (true) { // Process items from the priority queue @@ -115,6 +116,9 @@ public static async Task ProfileCheckTask(ConcurrentPriorityQueue userGroups, QueuedProcess item ) + private async static void GetUserGroupInformation(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, List userGroups, QueuedProcess item) { logger.Debug($"Processing User Group subscription for userId: {item.UserId}"); bool isSuspectGroup = false; @@ -189,13 +189,16 @@ private async static void GetUserGroupInformation(ServiceRegistry serviceRegistr } if (isSuspectGroup) - { + { Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(item.UserId ?? string.Empty); if (player != null) { player.IsGroupWatch = true; serviceRegistry.GetPlayerManager().OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); } + + string? soundSetting = ConfigStore.LoadSecret(Common.Common.Registry_Alert_Group) ?? "Hand"; + SoundManager.PlaySound(soundSetting); } } @@ -273,9 +276,9 @@ private static void GetEvaluationFromStore(ServiceRegistry serviceRegistry, Prof logger.Debug($"User profile lookup fails for a null userId"); } } - - private static bool IsEvaluated( string? evaluated ) + + private static bool IsEvaluated(string? evaluated) { if (string.IsNullOrEmpty(evaluated)) { @@ -286,7 +289,11 @@ private static bool IsEvaluated( string? evaluated ) CheckLines(evaluated, "Harrassment & Bullying") || CheckLines(evaluated, "Self Harm")) { - return true; + + string? soundSetting = ConfigStore.LoadSecret(Common.Common.Registry_Alert_Profile) ?? "Hand"; + SoundManager.PlaySound(soundSetting); + + return true; } return false; diff --git a/src/Clients/VRChat/VRChat.cs b/src/Clients/VRChat/VRChat.cs index 60dd163..31626b1 100644 --- a/src/Clients/VRChat/VRChat.cs +++ b/src/Clients/VRChat/VRChat.cs @@ -4,8 +4,8 @@ using System.IO; using System.Net; using Tailgrab.Config; -using VRChat.API.Client; using VRChat.API.Model; +using VRChat.API.Client; namespace Tailgrab.Clients.VRChat @@ -27,7 +27,7 @@ public async void Initialize() System.Windows.MessageBox.Show("VR Chat Web API Credentials are not set yet, use the Config / Secrets tab to update credenials and restart Tailgrab.", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); return; } - + string cookiePath = Path.Combine(Directory.GetCurrentDirectory(), "cookies.json"); // Try to load cookies from disk and use them if they are present and not expired @@ -157,7 +157,7 @@ public List GetAvatarsByUserId(string userId) { List avatars = new List(); try - { + { if (_vrchat != null) { avatars = _vrchat.Avatars.SearchAvatars(sort: SortOption.Order, order: OrderOption.Descending, userId: userId, tag: "avatargallery"); @@ -213,12 +213,12 @@ public List GetProfileGroups(string userId) Print? printInfo = null; try { - if( _vrchat != null) + if (_vrchat != null) { printInfo = _vrchat.Prints.GetPrint(fileURL); logger.Info($"Fetched print info: {printInfo?.Id} by {printInfo?.AuthorName}"); } - } + } catch (Exception ex) { logger.Error($"Error fetching avatar: {ex.Message}"); @@ -253,6 +253,24 @@ private static void SaveCookiesToFile(string filePath, List cookies) System.IO.File.WriteAllText(filePath, json); } + internal Group? getGroupById(string id) + { + Group? group = null; + try + { + if (_vrchat != null) + { + group = _vrchat.Groups.GetGroup(id); + } + } + catch (Exception ex) + { + logger.Error($"Error fetching Group information: {ex.Message}"); + } + + return group; + } + private class SerializableCookie { public string Name { get; set; } = string.Empty; diff --git a/src/Common/Checksum.cs b/src/Common/Checksum.cs index cd283d9..e956cc6 100644 --- a/src/Common/Checksum.cs +++ b/src/Common/Checksum.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Tailgrab.Common +namespace Tailgrab.Common { internal class Checksum { diff --git a/src/Common/Common.cs b/src/Common/Common.cs index 14d0e99..2ffc066 100644 --- a/src/Common/Common.cs +++ b/src/Common/Common.cs @@ -5,9 +5,13 @@ public static class Common public const string ApplicationName = "Tailgrab"; public const string CompanyName = "DeviousFox"; public const string ConfigRegistryPath = "Software\\DeviousFox\\Tailgrab\\Config"; + + // VRChat Web API registry keys public const string Registry_VRChat_Web_UserName = "VRCHAT_USERNAME"; public const string Registry_VRChat_Web_Password = "VRCHAT_PASSWORD"; public const string Registry_VRChat_Web_2FactorKey = "VRCHAT_2FA"; + + // Ollama API registry keys and defaults public const string Registry_Ollama_API_Key = "OLLAMA_API_KEY"; public const string Registry_Ollama_API_Endpoint = "OLLAMA_API_ENDPOINT"; public const string Registry_Ollama_API_Prompt = "OLLAMA_API_PROMPT"; @@ -16,5 +20,10 @@ public static class Common public const string Default_Ollama_API_Endpoint = "https://ollama.com"; public const string Default_Ollama_API_Model = "gemma3:27b"; + // Alert sound registry keys + public const string Registry_Alert_Avatar = "ALERT_AVATAR_SOUND"; + public const string Registry_Alert_Group = "ALERT_GROUP_SOUND"; + public const string Registry_Alert_Profile = "ALERT_PROFILE_SOUND"; + } } diff --git a/src/Common/SoundManager.cs b/src/Common/SoundManager.cs new file mode 100644 index 0000000..248523f --- /dev/null +++ b/src/Common/SoundManager.cs @@ -0,0 +1,205 @@ +using NLog; +using System.IO; +using System.Media; +using System.Windows.Media; +using System.Windows.Threading; + +namespace Tailgrab.Common +{ + /// + /// Central sound playback helper. Call from anywhere in the solution: + /// SoundManager.PlaySound("Asterisk"); + /// or + /// SoundManager.PlaySound("notification"); + /// which will look for ./sounds/notification.wav|.mp3|.ogg + /// + public static class SoundManager + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static readonly string[] allSystemSounds = { "Asterisk", "Beep", "Exclamation", "Warning", "Hand", "Error", "Question" }; + + /// + /// Enumerate available sound base filenames (without extension) from the ./sounds directory. + /// Looks for files with extensions: .wav, .mp3, .ogg and returns a distinct, ordered list. + /// + public static List GetAvailableSounds() + { + try + { + var baseDir = AppContext.BaseDirectory ?? Directory.GetCurrentDirectory(); + var soundsDir = Path.Combine(baseDir, "sounds"); + if (!Directory.Exists(soundsDir)) + { + return new List(); + } + + var exts = new[] { ".wav", ".mp3", ".ogg" }; + var files = Directory.EnumerateFiles(soundsDir) + .Where(f => exts.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase)) + .Select(f => Path.GetFileNameWithoutExtension(f)) + .Where(n => !string.IsNullOrWhiteSpace(n)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(n => n, StringComparer.OrdinalIgnoreCase) + .ToList(); + + files = allSystemSounds + .Concat(files) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + return files; + } + catch (Exception ex) + { + Logger.Warn(ex, "Failed to enumerate sounds directory"); + return new List(); + } + } + + /// + /// Play a system alert sound or a file under the local "sounds" directory. + /// Recognised system names (case-insensitive): Asterisk, Beep, Exclamation, Hand, Question + /// If the name does not match a system sound, it is treated as a base filename and looked up + /// under the "sounds" directory (AppContext.BaseDirectory + "sounds"). Supported extensions + /// (checked in order): .wav, .mp3, .ogg + /// + /// System code or base filename + public static void PlaySound(string? name) + { + if (string.IsNullOrWhiteSpace(name)) return; + + // System sounds + switch (name.Trim().ToLowerInvariant()) + { + case "asterisk": + SystemSounds.Asterisk.Play(); + return; + case "beep": + SystemSounds.Beep.Play(); + return; + case "exclamation": + case "warning": + SystemSounds.Exclamation.Play(); + return; + case "hand": + case "error": + SystemSounds.Hand.Play(); + return; + case "question": + SystemSounds.Question.Play(); + return; + } + + // Treat as filename under ./sounds + try + { + var baseDir = AppContext.BaseDirectory ?? Directory.GetCurrentDirectory(); + var soundsDir = Path.Combine(baseDir, "sounds"); + + string candidate = name; + // If an absolute or relative path was passed, respect it + if (Path.IsPathRooted(candidate)) + { + if (File.Exists(candidate)) + { + PlayFile(candidate); + return; + } + } + else + { + // search for file with supported extensions + var exts = new[] { ".wav", ".mp3", ".ogg" }; + foreach (var ext in exts) + { + var path = Path.Combine(soundsDir, candidate + ext); + if (File.Exists(path)) + { + PlayFile(path); + return; + } + } + + // also allow candidate to already include extension inside sounds dir + var direct = Path.Combine(soundsDir, candidate); + if (File.Exists(direct)) + { + PlayFile(direct); + return; + } + } + + Logger.Warn($"Sound file not found for '{name}' in '{soundsDir}'"); + } + catch (Exception ex) + { + Logger.Warn(ex, $"Failed to play sound '{name}'"); + } + } + + private static void PlayFile(string path) + { + var ext = Path.GetExtension(path).ToLowerInvariant(); + if (ext == ".wav") + { + try + { + // SoundPlayer supports WAV playback simply + Task.Run(() => + { + try + { + using var sp = new SoundPlayer(path); + sp.Play(); + } + catch (Exception ex) + { + Logger.Warn(ex, $"Failed to play wav '{path}'"); + } + }); + } + catch (Exception ex) + { + Logger.Warn(ex, $"Failed to start playback for wav '{path}'"); + } + } + else + { + // For mp3/ogg attempt to play using WPF MediaPlayer on an STA thread with its own Dispatcher + var t = new Thread(() => + { + try + { + var player = new MediaPlayer(); + player.Open(new Uri(path)); + + // When playback ends or fails, shutdown the dispatcher to allow thread to exit + player.MediaEnded += (_, __) => + { + try { player.Close(); } catch { } + Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Background); + }; + player.MediaFailed += (_, __) => + { + try { player.Close(); } catch { } + Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Background); + }; + + player.Play(); + + // Start a dispatcher loop to service MediaPlayer events + Dispatcher.Run(); + } + catch (Exception ex) + { + Logger.Warn(ex, $"Failed to play media '{path}'"); + } + }); + + t.IsBackground = true; + t.SetApartmentState(ApartmentState.STA); + t.Start(); + } + } + } +} diff --git a/src/Config/ConfigStore.cs b/src/Config/ConfigStore.cs index 5b2e04c..87d543f 100644 --- a/src/Config/ConfigStore.cs +++ b/src/Config/ConfigStore.cs @@ -1,5 +1,4 @@ using Microsoft.Win32; -using System; using System.Security.Cryptography; using System.Text; diff --git a/src/LineHandlers/AbstractLineHandler.cs b/src/LineHandlers/AbstractLineHandler.cs index a4064f8..664e281 100644 --- a/src/LineHandlers/AbstractLineHandler.cs +++ b/src/LineHandlers/AbstractLineHandler.cs @@ -8,14 +8,14 @@ namespace Tailgrab.LineHandler public interface ILineHandler { void AddAction(IAction action); - + bool HandleLine(string line); - void LogOutputColor(AnsiColor color ); + void LogOutputColor(AnsiColor color); } - public abstract class AbstractLineHandler: ILineHandler + public abstract class AbstractLineHandler : ILineHandler { private string _Pattern; @@ -24,26 +24,30 @@ public abstract class AbstractLineHandler: ILineHandler public virtual string Pattern { - get { - return _Pattern; + get + { + return _Pattern; } - set { + set + { _Pattern = value; regex = new Regex(_Pattern); - } + } } public virtual ServiceRegistry ServiceRegistry { - get { - return _serviceRegistry; + get + { + return _serviceRegistry; } - set { - if( value == null) + set + { + if (value == null) { throw new ArgumentNullException("ServiceRegistry cannot be null"); } - _serviceRegistry = value; + _serviceRegistry = value; } } @@ -84,5 +88,5 @@ void ILineHandler.LogOutputColor(AnsiColor color) LogOutputColor = color; } } - + } diff --git a/src/LineHandlers/AvatarChangeHandler.cs b/src/LineHandlers/AvatarChangeHandler.cs index f630387..1da722c 100644 --- a/src/LineHandlers/AvatarChangeHandler.cs +++ b/src/LineHandlers/AvatarChangeHandler.cs @@ -16,18 +16,18 @@ public class AvatarChangeHandler : AbstractLineHandler public AvatarChangeHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** AvatarChange Handler: Regular Expression: {Pattern}"); + logger.Info($"** AvatarChange Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string userName = m.Groups[VRC_DISPLAYNAME].Value; string avatarName = m.Groups[VRC_AVATARNAME].Value; - if( LogOutput ) + if (LogOutput) { logger.Info($"{COLOR_PREFIX}Avatar Change : {userName} to {avatarName}{COLOR_RESET.GetAnsiEscape()}"); } diff --git a/src/LineHandlers/AvatarUnpackHandler.cs b/src/LineHandlers/AvatarUnpackHandler.cs index 4126567..fd46063 100644 --- a/src/LineHandlers/AvatarUnpackHandler.cs +++ b/src/LineHandlers/AvatarUnpackHandler.cs @@ -1,7 +1,7 @@ namespace Tailgrab.LineHandler; -using Tailgrab.Common; using System.Text.RegularExpressions; +using Tailgrab.Common; public class AvatarUnpackHandler : AbstractLineHandler { @@ -15,18 +15,18 @@ public class AvatarUnpackHandler : AbstractLineHandler public AvatarUnpackHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** AvatarUnpack Handler: Regular Expression: {Pattern}"); + logger.Info($"** AvatarUnpack Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string userName = m.Groups[VRC_DISPLAYNAME].Value; string avatarName = m.Groups[VRC_AVATARNAME].Value; - if( LogOutput ) + if (LogOutput) { logger.Info($"{COLOR_PREFIX}Avatar Unpack : {avatarName} by {userName}{COLOR_RESET.GetAnsiEscape()}"); } diff --git a/src/LineHandlers/EmojiHandler.cs b/src/LineHandlers/EmojiHandler.cs index 4a5d112..0ac4bb7 100644 --- a/src/LineHandlers/EmojiHandler.cs +++ b/src/LineHandlers/EmojiHandler.cs @@ -2,7 +2,6 @@ namespace Tailgrab.LineHandler; using System.Text.RegularExpressions; using Tailgrab.Common; -using Tailgrab.PlayerManagement; public class EmojiHandler : AbstractLineHandler { @@ -15,18 +14,18 @@ public class EmojiHandler : AbstractLineHandler public EmojiHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** Emoji Handler: Regular Expression: {Pattern}"); + logger.Info($"** Emoji Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string fileURL = m.Groups[VRC_FILEURL].Value; - _serviceRegistry.GetPlayerManager().AddInventorySpawn( fileURL ); - if ( LogOutput ) + _serviceRegistry.GetPlayerManager().AddInventorySpawn(fileURL); + if (LogOutput) { logger.Info($"{COLOR_PREFIX}Print : {fileURL}{COLOR_RESET.GetAnsiEscape()}"); } diff --git a/src/LineHandlers/LoggingLineHandler.cs b/src/LineHandlers/LoggingLineHandler.cs index ebac52e..04b656f 100644 --- a/src/LineHandlers/LoggingLineHandler.cs +++ b/src/LineHandlers/LoggingLineHandler.cs @@ -1,6 +1,7 @@ using Tailgrab.Common; namespace Tailgrab.LineHandler; + public class LoggingLineHandler : AbstractLineHandler { public LoggingLineHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) @@ -11,7 +12,7 @@ public override bool HandleLine(string line) { if (regex.IsMatch(line)) { - if( LogOutput ) + if (LogOutput) { logger.Info($"{COLOR_PREFIX}{line}{COLOR_RESET.GetAnsiEscape()}"); } diff --git a/src/LineHandlers/OnPlayerJoinHandler.cs b/src/LineHandlers/OnPlayerJoinHandler.cs index 5352cbc..37b0da1 100644 --- a/src/LineHandlers/OnPlayerJoinHandler.cs +++ b/src/LineHandlers/OnPlayerJoinHandler.cs @@ -15,13 +15,13 @@ public class OnPlayerJoinHandler : AbstractLineHandler public OnPlayerJoinHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** OnPlayer Join/Leave Handler: Regular Expression: {Pattern}"); + logger.Info($"** OnPlayer Join/Leave Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string action = m.Groups[VRC_ACTION].Value; @@ -29,11 +29,11 @@ public override bool HandleLine(string line) string userId = m.Groups[VRC_USERID].Value; ExecuteActions(); - if( action.Equals("Joined") ) + if (action.Equals("Joined")) { - _serviceRegistry.GetPlayerManager().PlayerJoined(userId, userName, this ); + _serviceRegistry.GetPlayerManager().PlayerJoined(userId, userName, this); } - else if( action.Equals("Left") ) + else if (action.Equals("Left")) { _serviceRegistry.GetPlayerManager().PlayerLeft(userName, this); } diff --git a/src/LineHandlers/OnPlayerNetworkHandler.cs b/src/LineHandlers/OnPlayerNetworkHandler.cs index 72cae86..5a9d42d 100644 --- a/src/LineHandlers/OnPlayerNetworkHandler.cs +++ b/src/LineHandlers/OnPlayerNetworkHandler.cs @@ -1,7 +1,6 @@ namespace Tailgrab.LineHandler; using System.Text.RegularExpressions; -using Tailgrab.PlayerManagement; public class OnPlayerNetworkHandler : AbstractLineHandler { @@ -15,28 +14,28 @@ public class OnPlayerNetworkHandler : AbstractLineHandler public OnPlayerNetworkHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** OnPlayer Network ID Handler: Regular Expression: {Pattern}"); + logger.Info($"** OnPlayer Network ID Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string userName = m.Groups[VRC_DISPLAYNAME].Value; - int networkId = int.Parse( m.Groups[VRC_NETWORKID].Value); - if( LogOutput ) + int networkId = int.Parse(m.Groups[VRC_NETWORKID].Value); + if (LogOutput) { logger.Info($"{COLOR_PREFIX}Network_ID : {userName} ({networkId}){COLOR_RESET}"); } ExecuteActions(); - _serviceRegistry.GetPlayerManager().AssignPlayerNetworkId(userName, networkId ); + _serviceRegistry.GetPlayerManager().AssignPlayerNetworkId(userName, networkId); return true; } - + return false; } } \ No newline at end of file diff --git a/src/LineHandlers/PenNetworkIdHandler.cs b/src/LineHandlers/PenNetworkIdHandler.cs index d3fcab1..d0314ec 100644 --- a/src/LineHandlers/PenNetworkIdHandler.cs +++ b/src/LineHandlers/PenNetworkIdHandler.cs @@ -20,12 +20,12 @@ public class PenNetworkHandler : AbstractLineHandler public PenNetworkHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** Pen Network Id Handler: Regular Expression: {Pattern}"); + logger.Info($"** Pen Network Id Handler: Regular Expression: {Pattern}"); using (FileStream fs = new FileStream("./pen-network-id.csv", FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (StreamReader sr = new StreamReader(fs, Encoding.UTF8)) { - Console.WriteLine($"Loading Pen Network ID mappings..."); + Console.WriteLine($"Loading Pen Network ID mappings..."); while (true && sr != null) { string? line = sr.ReadLine(); @@ -36,40 +36,40 @@ public PenNetworkHandler(string matchPattern, ServiceRegistry serviceRegistry) : string[] parts = line.Split(','); int networkId = int.Parse(parts[0]); - string penColor = parts[1]; + string penColor = parts[1]; penNetworkMap[networkId] = penColor; logger.Debug($"Mapped Pen Color '{penColor}' to Network ID: {networkId}"); - } + } } } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; - int objectId = int.Parse( m.Groups[VRC_OBJECT_NETWORK_ID].Value ); - int fromUserId = int.Parse( m.Groups[VRC_FROM_NETWORK_ID].Value ); - int toUserId = int.Parse( m.Groups[VRC_TO_NETWORK_ID].Value ); + int objectId = int.Parse(m.Groups[VRC_OBJECT_NETWORK_ID].Value); + int fromUserId = int.Parse(m.Groups[VRC_FROM_NETWORK_ID].Value); + int toUserId = int.Parse(m.Groups[VRC_TO_NETWORK_ID].Value); if (penNetworkMap.TryGetValue(objectId, out string? penColor)) { string fromPlayerName = "Unknown"; string toPlayerName = "Unknown"; - if( _serviceRegistry.GetPlayerManager().GetPlayerByNetworkId(fromUserId) is Player fromPlayer ) + if (_serviceRegistry.GetPlayerManager().GetPlayerByNetworkId(fromUserId) is Player fromPlayer) { fromPlayerName = fromPlayer.DisplayName; _serviceRegistry.GetPlayerManager().AddPenEventByDisplayName(fromPlayerName, $"-Pen '{penColor}'."); _serviceRegistry.GetPlayerManager().AddPlayerEventByDisplayName(fromPlayerName, PlayerEvent.EventType.PenActivity, $"Lost ownership of pen '{penColor}'."); } - if( _serviceRegistry.GetPlayerManager().GetPlayerByNetworkId(toUserId) is Player toPlayer ) + if (_serviceRegistry.GetPlayerManager().GetPlayerByNetworkId(toUserId) is Player toPlayer) { toPlayerName = toPlayer.DisplayName; _serviceRegistry.GetPlayerManager().AddPenEventByDisplayName(toPlayerName, $"+Pen '{penColor}'."); _serviceRegistry.GetPlayerManager().AddPlayerEventByDisplayName(toPlayerName, PlayerEvent.EventType.PenActivity, $"Took ownership of pen '{penColor}'."); } - if( LogOutput ) + if (LogOutput) { logger.Info($"Pen '{penColor}' Ownership Change : {fromPlayerName} to {toPlayerName}"); } diff --git a/src/LineHandlers/PrintHandler.cs b/src/LineHandlers/PrintHandler.cs index 4ec8c32..e3df161 100644 --- a/src/LineHandlers/PrintHandler.cs +++ b/src/LineHandlers/PrintHandler.cs @@ -2,7 +2,6 @@ namespace Tailgrab.LineHandler; using System.Text.RegularExpressions; using Tailgrab.Common; -using Tailgrab.PlayerManagement; public class PrintHandler : AbstractLineHandler { @@ -15,18 +14,18 @@ public class PrintHandler : AbstractLineHandler public PrintHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** Print Handler: Regular Expression: {Pattern}"); + logger.Info($"** Print Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string fileURL = m.Groups[VRC_FILEURL].Value; - _serviceRegistry.GetPlayerManager().AddPrintData( fileURL ); - if ( LogOutput ) + _serviceRegistry.GetPlayerManager().AddPrintData(fileURL); + if (LogOutput) { logger.Info($"{COLOR_PREFIX}Print : {fileURL}{COLOR_RESET.GetAnsiEscape()}"); } diff --git a/src/LineHandlers/QuitHandler.cs b/src/LineHandlers/QuitHandler.cs index 6426b2e..35c6bda 100644 --- a/src/LineHandlers/QuitHandler.cs +++ b/src/LineHandlers/QuitHandler.cs @@ -14,13 +14,13 @@ public class QuitHandler : AbstractLineHandler public QuitHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** VRC Quit Handler: Regular Expression: {Pattern}"); + logger.Info($"** VRC Quit Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string totalTime = m.Groups[VRC_TOTALSEC].Value; @@ -33,7 +33,7 @@ public override bool HandleLine(string line) //int minutes = time.Minutes; //int seconds = time.Seconds; - if ( LogOutput ) + if (LogOutput) { //string formattedTime = string.Format("{0:D2}:{1:D2}:{2:D2}", time.Hours, time.Minutes, time.Seconds); logger.Info($"{COLOR_PREFIX}Application Stop : {totalTime} seconds{COLOR_RESET.GetAnsiEscape()}"); diff --git a/src/LineHandlers/StickerHandler.cs b/src/LineHandlers/StickerHandler.cs index f3a435d..be9937b 100644 --- a/src/LineHandlers/StickerHandler.cs +++ b/src/LineHandlers/StickerHandler.cs @@ -2,7 +2,6 @@ namespace Tailgrab.LineHandler; using System.Text.RegularExpressions; using Tailgrab.Common; -using Tailgrab.PlayerManagement; public class StickerHandler : AbstractLineHandler { @@ -17,23 +16,23 @@ public class StickerHandler : AbstractLineHandler public StickerHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** Sticker Handler: Regular Expression: {Pattern}"); + logger.Info($"** Sticker Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string fileURL = m.Groups[VRC_FILEURL].Value; string userName = m.Groups[VRC_DISPLAYNAME].Value; string userId = m.Groups[VRC_USERID].Value; - if( LogOutput ) + if (LogOutput) { logger.Info($"{COLOR_PREFIX}{userName} ({userId}) - {fileURL}{COLOR_RESET.GetAnsiEscape()}"); } - _serviceRegistry.GetPlayerManager().AddStickerEvent( userName, userId, fileURL ); + _serviceRegistry.GetPlayerManager().AddStickerEvent(userName, userId, fileURL); ExecuteActions(); return true; diff --git a/src/LineHandlers/VTKHandler.cs b/src/LineHandlers/VTKHandler.cs index 98f23ac..e308f2b 100644 --- a/src/LineHandlers/VTKHandler.cs +++ b/src/LineHandlers/VTKHandler.cs @@ -21,16 +21,16 @@ public VTKHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(m public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string userName = m.Groups[VRC_DISPLAYNAME].Value; - if( LogOutput ) + if (LogOutput) { logger.Info($"{COLOR_PREFIX}VTK : {userName}{COLOR_RESET.GetAnsiEscape()}"); } - _serviceRegistry.GetPlayerManager().AddPlayerEventByDisplayName(userName, PlayerEvent.EventType.Moderation, "Vote kick initiated against player."); + _serviceRegistry.GetPlayerManager().AddPlayerEventByDisplayName(userName, PlayerEvent.EventType.Moderation, "Vote kick initiated against player."); ExecuteActions(); return true; diff --git a/src/LineHandlers/WarnKickHandler.cs b/src/LineHandlers/WarnKickHandler.cs index c743881..0a92c99 100644 --- a/src/LineHandlers/WarnKickHandler.cs +++ b/src/LineHandlers/WarnKickHandler.cs @@ -16,18 +16,18 @@ public class WarnKickHandler : AbstractLineHandler public WarnKickHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** Moderation Warn/Kick Handler: Regular Expression: {Pattern}"); + logger.Info($"** Moderation Warn/Kick Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string userName = m.Groups[VRC_DISPLAYNAME].Value; string action = m.Groups[VRC_ACTION].Value; - if( LogOutput ) + if (LogOutput) { logger.Info($"{COLOR_PREFIX}User Moderation : {userName} to {action}{COLOR_RESET.GetAnsiEscape()}"); } diff --git a/src/LineHandlers/WorldChangeHandler.cs b/src/LineHandlers/WorldChangeHandler.cs index b4dac2b..ba97812 100644 --- a/src/LineHandlers/WorldChangeHandler.cs +++ b/src/LineHandlers/WorldChangeHandler.cs @@ -15,18 +15,18 @@ public class WorldChangeHandler : AbstractLineHandler public WorldChangeHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** World Join Handler: Regular Expression: {Pattern}"); + logger.Info($"** World Join Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) { Match m = regex.Match(line); - if( m.Success ) + if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; string worldId = m.Groups[VRC_WORLDID].Value; string instanceId = m.Groups[VRC_INSTANCEID].Value; - if( LogOutput ) + if (LogOutput) { logger.Info($"{COLOR_PREFIX}World Join : {worldId} as instance {instanceId}{COLOR_RESET.GetAnsiEscape()}"); } diff --git a/src/Models/TailgrabDBContext.cs b/src/Models/TailgrabDBContext.cs index ed84e4d..1b15339 100644 --- a/src/Models/TailgrabDBContext.cs +++ b/src/Models/TailgrabDBContext.cs @@ -65,7 +65,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.ToTable("UserInfo"); entity.Property(e => e.CreatedAt).IsRequired(); - entity.Property(e => e.ElapsedHours).HasColumnName("elapsedHours"); + entity.Property(e => e.ElapsedMinutes).HasColumnName("elapsedHours"); entity.Property(e => e.IsBos).HasColumnName("IsBOS"); }); diff --git a/src/Models/UserInfo.cs b/src/Models/UserInfo.cs index d33add3..aa533ce 100644 --- a/src/Models/UserInfo.cs +++ b/src/Models/UserInfo.cs @@ -12,7 +12,7 @@ public partial class UserInfo public string DisplayName { get; set; } - public double ElapsedHours { get; set; } + public double ElapsedMinutes { get; set; } public DateTime CreatedAt { get; set; } @@ -26,6 +26,6 @@ public UserInfo() public override string ToString() { - return $"UserInfo: {UserId}, DisplayName: {DisplayName}, ElapsedHours: {ElapsedHours}, CreatedAt: {CreatedAt}, UpdatedAt: {UpdatedAt}"; + return $"UserInfo: {UserId}, DisplayName: {DisplayName}, ElapsedMinutes: {ElapsedMinutes}, CreatedAt: {CreatedAt}, UpdatedAt: {UpdatedAt}"; } } \ No newline at end of file diff --git a/src/PlayerManagement/PlayerManagement.cs b/src/PlayerManagement/PlayerManagement.cs index 346f3e5..df6dd5b 100644 --- a/src/PlayerManagement/PlayerManagement.cs +++ b/src/PlayerManagement/PlayerManagement.cs @@ -48,9 +48,11 @@ public class Player public Dictionary PrintData = new Dictionary(); public string? UserBio { get; set; } public string? AIEval { get; set; } - public bool IsWatched { get - { - if( IsAvatarWatch || IsGroupWatch || IsProfileWatch) + public bool IsWatched + { + get + { + if (IsAvatarWatch || IsGroupWatch || IsProfileWatch) { return true; } @@ -61,19 +63,19 @@ public bool IsWatched { get public string WatchCode { - get - { + get + { string code = ""; - if( IsAvatarWatch ) + if (IsAvatarWatch) { code += "A"; - } - if( IsGroupWatch ) + } + if (IsGroupWatch) { code += "G"; } - if( IsProfileWatch ) + if (IsProfileWatch) { code += "P"; } @@ -87,7 +89,7 @@ public string WatchCode public bool IsProfileWatch { get; set; } = false; - public Player(string userId, string displayName, SessionInfo session ) + public Player(string userId, string displayName, SessionInfo session) { UserId = userId; DisplayName = displayName; @@ -144,7 +146,7 @@ public string ToString(bool full) } } - if ( full && UserBio != null && UserBio.Length > 0) + if (full && UserBio != null && UserBio.Length > 0) { sb.AppendLine(new string('-', 50)); @@ -259,7 +261,7 @@ public void PlayerJoined(string userId, string displayName, AbstractLineHandler changeType = PlayerChangedEventArgs.ChangeType.Added; } - if ( player == null ) + if (player == null) { logger.Error("PlayerJoined: Failed to create or retrieve player instance."); return; @@ -285,7 +287,7 @@ public void PlayerJoined(string userId, string displayName, AbstractLineHandler OnPlayerChanged(changeType, player); } - public void PlayerLeft(string displayName, AbstractLineHandler handler ) + public void PlayerLeft(string displayName, AbstractLineHandler handler) { if (playersByDisplayName.TryGetValue(displayName, out Player? player)) { @@ -302,7 +304,7 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler ) user.IsBos = 0; user.CreatedAt = DateTime.Now; user.UpdatedAt = DateTime.Now; - user.ElapsedHours = timeDifference.TotalMinutes; + user.ElapsedMinutes = timeDifference.TotalMinutes; dBContext.Add(user); dBContext.SaveChanges(); } @@ -310,7 +312,7 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler ) { user.DisplayName = player.DisplayName; user.UpdatedAt = DateTime.Now; - user.ElapsedHours = user.ElapsedHours + timeDifference.TotalMinutes; + user.ElapsedMinutes = user.ElapsedMinutes + timeDifference.TotalMinutes; dBContext.Update(user); dBContext.SaveChanges(); } @@ -321,7 +323,7 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler ) playersByDisplayName.Remove(displayName); playersByNetworkId.Remove(player.NetworkId); playersByUserId.Remove(player.UserId); - if( handler.LogOutput ) + if (handler.LogOutput) { PrintPlayerInfo(player); } @@ -348,12 +350,12 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler ) public Player? AssignPlayerNetworkId(string displayName, int networkId) { - if( playersByDisplayName.TryGetValue(displayName, out Player? player)) + if (playersByDisplayName.TryGetValue(displayName, out Player? player)) { player.NetworkId = networkId; playersByNetworkId[networkId] = player; OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); - } + } return player; } @@ -365,15 +367,10 @@ public IEnumerable GetAllPlayers() public void ClearAllPlayers(AbstractLineHandler handler) { - foreach (string avatarName in avatarsInSession) - { - serviceRegistry.GetAvatarManager().AddAvatarsInSession(avatarName); - } - - foreach ( var player in playersByUserId.Values ) + foreach (var player in playersByUserId.Values) { player.InstanceEndTime = DateTime.Now; - if( handler.LogOutput ) + if (handler.LogOutput) { PrintPlayerInfo(player); } @@ -387,34 +384,34 @@ public void ClearAllPlayers(AbstractLineHandler handler) avatarsInSession.Clear(); // Also a global cleared notification (consumers may want to reset) - OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Cleared, new Player("","", CurrentSession) { InstanceStartTime = DateTime.MinValue }); + OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Cleared, new Player("", "", CurrentSession) { InstanceStartTime = DateTime.MinValue }); } public int GetPlayerCount() { return playersByUserId.Count; - } + } public void LogAllPlayers(AbstractLineHandler handler) { - if( handler.LogOutput ) + if (handler.LogOutput) { - foreach( var player in playersByUserId.Values ) + foreach (var player in playersByUserId.Values) { PrintPlayerInfo(player); - } + } } } public Player? AddPlayerEventByDisplayName(string displayName, PlayerEvent.EventType eventType, string eventDescription) { - if( playersByDisplayName.TryGetValue(displayName, out Player? player)) + if (playersByDisplayName.TryGetValue(displayName, out Player? player)) { PlayerEvent newEvent = new PlayerEvent(eventType, eventDescription); player.AddEvent(newEvent); OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); return player; - } + } return null; } @@ -435,7 +432,7 @@ public void LogAllPlayers(AbstractLineHandler handler) public void SetAvatarForPlayer(string displayName, string avatarName) { bool watchedAvatar = serviceRegistry.GetAvatarManager().CheckAvatarByName(avatarName); - if( watchedAvatar ) + if (watchedAvatar) { logger.Info($"{COLOR_PREFIX_LEAVE.GetAnsiEscape()}Watched Avatar Detected for Player {displayName}: {avatarName}{COLOR_RESET.GetAnsiEscape()}"); } @@ -448,7 +445,7 @@ public void SetAvatarForPlayer(string displayName, string avatarName) OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, p); } - if( !avatarsInSession.Contains(avatarName)) + if (!avatarsInSession.Contains(avatarName)) { avatarsInSession.Add(avatarName); } @@ -495,10 +492,10 @@ internal void AddPrintData(string printId) Print? printInfo = serviceRegistry.GetVRChatAPIClient().GetPrintInfo(printId); if (printInfo != null) { - if( playersByUserId.TryGetValue(printInfo.OwnerId, out Player? player)) + if (playersByUserId.TryGetValue(printInfo.OwnerId, out Player? player)) { player.PrintData[printId] = printInfo; - logger.Info($"Added Print {printId} for Player {player.DisplayName} (ID: {printInfo.OwnerId})" ); + logger.Info($"Added Print {printId} for Player {player.DisplayName} (ID: {printInfo.OwnerId})"); } AddPlayerEventByUserId(printInfo.OwnerId, PlayerEvent.EventType.Print, $"Dropped Print {printId}"); } diff --git a/src/PlayerManagement/TailgrabPannel.xaml b/src/PlayerManagement/TailgrabPannel.xaml index abfa7c1..7243cfe 100644 --- a/src/PlayerManagement/TailgrabPannel.xaml +++ b/src/PlayerManagement/TailgrabPannel.xaml @@ -339,7 +339,52 @@ - + + + + + + + +