diff --git a/Migrations/20260129210247_V1.0.9.Designer.cs b/Migrations/20260129210247_V1.0.9.Designer.cs new file mode 100644 index 0000000..0fe4ec7 --- /dev/null +++ b/Migrations/20260129210247_V1.0.9.Designer.cs @@ -0,0 +1,126 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Tailgrab.Models; + +#nullable disable + +namespace tailgrab.Migrations +{ + [DbContext(typeof(TailgrabDBContext))] + [Migration("20260129210247_V1.0.9")] + partial class V109 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("Tailgrab.Models.AvatarInfo", b => + { + b.Property("AvatarId") + .HasColumnType("TEXT"); + + b.Property("AvatarName") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("IsBos") + .HasColumnType("INTEGER") + .HasColumnName("IsBOS"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("AvatarId"); + + b.ToTable("AvatarInfo", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.GroupInfo", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("createDate"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("IsBos") + .HasColumnType("INTEGER") + .HasColumnName("IsBOS"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updateDate"); + + b.HasKey("GroupId"); + + b.ToTable("GroupInfo", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.ProfileEvaluation", b => + { + b.Property("Md5checksum") + .HasColumnType("TEXT") + .HasColumnName("MD5Checksum"); + + b.Property("Evaluation") + .HasColumnType("BLOB"); + + b.Property("LastDateTime") + .HasColumnType("TEXT"); + + b.Property("ProfileText") + .HasColumnType("BLOB"); + + b.HasKey("Md5checksum"); + + b.ToTable("ProfileEvaluation", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.UserInfo", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("ElapsedMinutes") + .HasColumnType("REAL") + .HasColumnName("elapsedHours"); + + b.Property("IsBos") + .HasColumnType("INTEGER") + .HasColumnName("IsBOS"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.ToTable("UserInfo", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260129210247_V1.0.9.cs b/Migrations/20260129210247_V1.0.9.cs new file mode 100644 index 0000000..ac91d06 --- /dev/null +++ b/Migrations/20260129210247_V1.0.9.cs @@ -0,0 +1,93 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace tailgrab.Migrations +{ + /// + public partial class V109 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AvatarInfo", + columns: table => new + { + AvatarId = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: true), + AvatarName = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + IsBOS = table.Column(type: "INTEGER", nullable: false), + ImageUrl = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AvatarInfo", x => x.AvatarId); + }); + + migrationBuilder.CreateTable( + name: "GroupInfo", + columns: table => new + { + GroupId = table.Column(type: "TEXT", nullable: false), + GroupName = table.Column(type: "TEXT", nullable: true), + IsBOS = table.Column(type: "INTEGER", nullable: false), + createDate = table.Column(type: "TEXT", nullable: false), + updateDate = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_GroupInfo", x => x.GroupId); + }); + + migrationBuilder.CreateTable( + name: "ProfileEvaluation", + columns: table => new + { + MD5Checksum = table.Column(type: "TEXT", nullable: false), + ProfileText = table.Column(type: "BLOB", nullable: true), + Evaluation = table.Column(type: "BLOB", nullable: true), + LastDateTime = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProfileEvaluation", x => x.MD5Checksum); + }); + + migrationBuilder.CreateTable( + name: "UserInfo", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: true), + elapsedHours = table.Column(type: "REAL", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + IsBOS = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserInfo", x => x.UserId); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AvatarInfo"); + + migrationBuilder.DropTable( + name: "GroupInfo"); + + migrationBuilder.DropTable( + name: "ProfileEvaluation"); + + migrationBuilder.DropTable( + name: "UserInfo"); + } + } +} diff --git a/Migrations/20260206180413_V1.0.10.Designer.cs b/Migrations/20260206180413_V1.0.10.Designer.cs new file mode 100644 index 0000000..910c04b --- /dev/null +++ b/Migrations/20260206180413_V1.0.10.Designer.cs @@ -0,0 +1,149 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Tailgrab.Models; + +#nullable disable + +namespace tailgrab.Migrations +{ + [DbContext(typeof(TailgrabDBContext))] + [Migration("20260206180413_V1.0.10")] + partial class V1010 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("Tailgrab.Models.AvatarInfo", b => + { + b.Property("AvatarId") + .HasColumnType("TEXT"); + + b.Property("AvatarName") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("IsBos") + .HasColumnType("INTEGER") + .HasColumnName("IsBOS"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("AvatarId"); + + b.ToTable("AvatarInfo", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.GroupInfo", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("createDate"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("IsBos") + .HasColumnType("INTEGER") + .HasColumnName("IsBOS"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updateDate"); + + b.HasKey("GroupId"); + + b.ToTable("GroupInfo", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.ImageEvaluation", b => + { + b.Property("InventoryId") + .HasColumnType("TEXT"); + + b.Property("Evaluation") + .HasColumnType("BLOB"); + + b.Property("LastDateTime") + .HasColumnType("TEXT"); + + b.Property("Md5checksum") + .HasColumnType("TEXT") + .HasColumnName("MD5Checksum"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("InventoryId"); + + b.ToTable("ImageEvaluation", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.ProfileEvaluation", b => + { + b.Property("Md5checksum") + .HasColumnType("TEXT") + .HasColumnName("MD5Checksum"); + + b.Property("Evaluation") + .HasColumnType("BLOB"); + + b.Property("LastDateTime") + .HasColumnType("TEXT"); + + b.Property("ProfileText") + .HasColumnType("BLOB"); + + b.HasKey("Md5checksum"); + + b.ToTable("ProfileEvaluation", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.UserInfo", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("ElapsedMinutes") + .HasColumnType("REAL") + .HasColumnName("elapsedHours"); + + b.Property("IsBos") + .HasColumnType("INTEGER") + .HasColumnName("IsBOS"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.ToTable("UserInfo", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260206180413_V1.0.10.cs b/Migrations/20260206180413_V1.0.10.cs new file mode 100644 index 0000000..8038fa0 --- /dev/null +++ b/Migrations/20260206180413_V1.0.10.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace tailgrab.Migrations +{ + /// + public partial class V1010 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ImageEvaluation", + columns: table => new + { + InventoryId = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: true), + MD5Checksum = table.Column(type: "TEXT", nullable: true), + Evaluation = table.Column(type: "BLOB", nullable: true), + LastDateTime = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ImageEvaluation", x => x.InventoryId); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ImageEvaluation"); + } + } +} diff --git a/Migrations/TailgrabDBContextModelSnapshot.cs b/Migrations/TailgrabDBContextModelSnapshot.cs new file mode 100644 index 0000000..3698cc8 --- /dev/null +++ b/Migrations/TailgrabDBContextModelSnapshot.cs @@ -0,0 +1,146 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Tailgrab.Models; + +#nullable disable + +namespace tailgrab.Migrations +{ + [DbContext(typeof(TailgrabDBContext))] + partial class TailgrabDBContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("Tailgrab.Models.AvatarInfo", b => + { + b.Property("AvatarId") + .HasColumnType("TEXT"); + + b.Property("AvatarName") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("IsBos") + .HasColumnType("INTEGER") + .HasColumnName("IsBOS"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("AvatarId"); + + b.ToTable("AvatarInfo", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.GroupInfo", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("createDate"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("IsBos") + .HasColumnType("INTEGER") + .HasColumnName("IsBOS"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updateDate"); + + b.HasKey("GroupId"); + + b.ToTable("GroupInfo", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.ImageEvaluation", b => + { + b.Property("InventoryId") + .HasColumnType("TEXT"); + + b.Property("Evaluation") + .HasColumnType("BLOB"); + + b.Property("LastDateTime") + .HasColumnType("TEXT"); + + b.Property("Md5checksum") + .HasColumnType("TEXT") + .HasColumnName("MD5Checksum"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("InventoryId"); + + b.ToTable("ImageEvaluation", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.ProfileEvaluation", b => + { + b.Property("Md5checksum") + .HasColumnType("TEXT") + .HasColumnName("MD5Checksum"); + + b.Property("Evaluation") + .HasColumnType("BLOB"); + + b.Property("LastDateTime") + .HasColumnType("TEXT"); + + b.Property("ProfileText") + .HasColumnType("BLOB"); + + b.HasKey("Md5checksum"); + + b.ToTable("ProfileEvaluation", (string)null); + }); + + modelBuilder.Entity("Tailgrab.Models.UserInfo", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("ElapsedMinutes") + .HasColumnType("REAL") + .HasColumnName("elapsedHours"); + + b.Property("IsBos") + .HasColumnType("INTEGER") + .HasColumnName("IsBOS"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.ToTable("UserInfo", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/README.md b/README.md index 092443a..67685ee 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,47 @@ -# TailGrab +# Tailgrab VRChat Log Parser and Automation tool to help moderators manage trouble makers in VRChat since VRChat Management Team is not taking moderation seriously; ever. -# Capabilities +Tailgrab will read the VRChat Local Game Log files in real time, parse them for events and then trigger actions based on the configuration of the application. The application is designed to be flexible and allow for a wide range of actions to be triggered based on the events that are parsed from the VRChat logs and alert the user to elements that may be less than honest. -The core concept of the TailGrab was to create a Windows friendly ```grep``` of VR Chat log events that would allow a group moderation team to review, get insights of bad actors and with the action framework to perform a scripted reaction to a VR Chat game log event. +[](./tailgrab_application.png) -EG: -A ```Vote To Kick``` is received from a patreon, the action sequence could: -- Send a OSC Avatar Parameter(s) that change the avatar's ear position -- Delay for a quarter of a second -- Send a keystroke to your soundboard application -- Send a keystroke to OBS to start recording +## Features +- Shows a live feed of user in the current instance with their VRChat Avatar and UserID +- When in Furry Hideout, shows user's usage of Furry Hideout Pens +- Quick view of the user's historical avatar, pen, print, emoji and sticker usage in the current instance. +- AI powered insights on user Profile content. +- Quick reporting of User's Profile to the in game moderation instance +- AI powered insights on user Image Assets used. +- Quick reporting of User's Images to the in game moderation instance +- Copy Button that copies the user's VRChat User Id, Display name, Instance Stats and historical activity for pasting into your favorite moderation toolset. +- Avatar Flagging based on user directed database. +- Group Flagging based on user directed database. +- Historical tracking of User elapsed time seen from your usage of the application. +- Trigger actions based on VRChat log events of "Vote To Kick" or "Group Moderation Action (Kick/Warn)", such as sending OSC Avatar Parameters, sending keystrokes to other applications, etc. -# Usage +## Installation + +> [!NOTE] +> I am learing how to build a Installer for the appliction, but for now you will need to download the latest release and extract the zip file to a location of your choice on your Windows machine. Then you can run the ```tailgrab.exe``` application to start monitoring your VRChat instance. + +### New Install +1. Download the latest release of TailGrab from the [Latest Release](https://github.com/jlong23/Tailgrab/releases/latest) page. +1. Extract the downloaded zip file to a location of your choice on your Windows machine. +1. Run the ```tailgrab.exe``` application to start monitoring your VRChat instance. + +### Updgrade from Previous Version +1. Download the latest release of TailGrab from the [Latest Release](https://github.com/jlong23/Tailgrab/releases/latest) page. +1. Extract the downloaded zip file to a location of your choice on your Windows machine, but avoid overwriting your existing configuration & data files. ./config.json ./pen-network-id.csv ./sounds/* ./data/* +1. Run the ```tailgrab.exe -upgrade``` application to start any database upgrades. +1. Configure any new Secrets and other configuration values that may be needed for new features added since your last version. +1. Restart the application with ```tailgrab.exe``` to start monitoring your VRChat instance with the new version of TailGrab. + +## Configuration +[Application Config](./docs/Config_Application.md) for details on how to configure the application to connect to API services. + +[Config Line Handlers](./docs/Config_LineHandlers.md) for details on how to configure the application to respond to VRChat local game log events. + +## Quick Usage Click the windows application or open a Powershell or Command Line prompt in your windows host, change directory to where ```tailgrab.exe``` has been extracted to and start it with: ```.\tailgrab.exe``` @@ -27,7 +56,7 @@ If you need to clear all registry settings stored for TailGrab, you can run: 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 +### VRChat Source Log Files By default TailGrab will look for VRChat log files in the default location of: @@ -37,7 +66,7 @@ This can be overridden by passing the full path to the VRChat log files as the f ```.\\tailgrab.exe D:\MyVRChatLogs\``` -## Watching TailGrab Application Logs +### Watching TailGrab Application Logs The TailGrab application will log it's internal operations to the ```./logs``` folder in the same directory as the application executable. Each run of the application will create a new log file with a timestamp in the filename. @@ -45,150 +74,28 @@ If you want to watch the application logs in real time, you can use a tool like ```Get-Content -Path .\logs\tailgrab-2026-01-26.log -wait``` -# Configuration - -## VR Chat and OLLama API Credentials - -Tailgrab uses VR Chat's public API to get information about avatars for the BlackListed Database (SQLite Local DB) and to get user profile infoformation for Profile Evaluation with the AI services. -OLLama Cloud AI services are used to evaluate user profiles for potential bad actors based on your custom prompt criteria. The OLLama API is called only once for a MD5 checksummed profile to reduce API usage and cost. - -The TailGrab application will look for the following credentials to connect to your VRChat API and OLLama AI services from the Windows Registry in a encyrpted format. On the first Run you may receive a Popup Message to set the values on the Config -> Secrets Tab and restart the application to get the services running properly. - -## Getting your VR Chat 2 Factor Authentication key - -I certainly hope you are using LastPass Authenticator or Google Authenticator to manage your 2FA codes for VRChat. If you are not, please stop reading this and go set that up now to protect your Online Accounts. - -On LastPass Authenticator for the your VR Chat Entry, you can use the right Hamburger menu icon to get a dialog of options, one of which is to 'Edit Account', select that and you will see the 'Secret Key' field, copy the 'Secret Key' value to your clipboard and paste to something you can transfer to your PC (Or tediously type it in from the screen). - -## "Config.json" File - -The confiuration for TailGrab uses a JSON formated payload of the base attribute "lineHandlers" that contains a array of LineHandler Objects, Those may have a attribute of "actions" that contain an array of Action Objects. This configuration is loaded on application start. - -## LineHandler Definition - -The LineHandler defines what type of system action to perform, what regular expression to use to detect that type of log line and user actions to perform when detected. - -|Attribute | Definition | -|--------|--------| -| handlerTypeValue | An enumeration value of the internal LineHandler code segments. See ```handlerTypes``` | -| enabled | Boolean ```true``` or ```false``` to direct the application to include or temporarly ignore the configuration. | -| patternTypeValue | An enumeration value of ```default``` or ```override```; Default will use the programmer's defined default for the Regular Expression to match/extract and a Override will allow the user to fine tune or respond to VRChat application log changes with the attribute ```pattern``` | -| pattern | The Regular expression for the Pattern to match/extract, does nothing unless patternTypeValue is set to override | -| logOutput | Boolean ```true``` or ```false``` to direct the application to log the output of the Line Handler. | -| logOutputColor | A value of ```Default``` will use the programmers ANSI codes for the log output, if you use the last digits of the ANSI codes here, they are used. EG ```"37m"``` | -| actions | A array of Action Configuration elements or do nothing by leaving it as an empty array ```[]``` | - -## actionTypeValue Enum Values - -|actionTypeValue | Definition | -|--------|--------| -| DelayAction | Delay a defined amount of time before next action. | -| OSCAction | Send OSC Avatar Parameter values to your VRChat Avatar. | -| KeyPressAction | Send Keystrokes to a named open window title on your system. | - - -## Action: DelayAction Definition - -The Delay Action will allow you to pause other actions with millisecond precision. If you need to pause for 1 second, use 1000 as the delay time. This action is used when there is a need for a sound trigger to play or you want to send stacked keystrokes to an application that is running. - -|Attribute | Definition | -|--------|--------| -| actionTypeValue | An enumeration value of the internal LineHandler code segments. See ```actionTypeValue``` | -| milliseconds | integer value of milliseconds to wait for. | - -## Action: OSCAction Definition - -The OSC Action will allow you to send values (```Float```/```Int```/```Bool```) to your VRChat avatar that could be used to trigger animations on it during a action set. +## Usefull Tool Sets -|Attribute | Definition | -|--------|--------| -| actionTypeValue | ```OSCAction``` See ```actionTypeValue``` | -| parameterName | The VRChat Avatar Parameter Path to send to; EG. ```/avatar/parameters/Ear/Right_Angle``` | -| oscValueType | OSC Value types associated with that Parameter Path; ```Float``` or ```Int``` or ```Bool``` | -| value | The Value to send to your avatar; Floats expect a decimal place ```0.0```, Int expect no decimal place ```0```, and Bool expects either ```true``` or ```false``` +DB Browser for SQLite - https://sqlitebrowser.org/ -## Action: TTSAction Definition -The TTS Action will allow you to say a phrase when triggered. +## Detail Documentation -|Attribute | Definition | -|--------|--------| -| actionTypeValue | ```TTSAction``` See ```actionTypeValue``` | -| text | The phrase you wish to have spoken | -| volume | Volume 0...100 | -| rate | The speed of the speech -10...10 | +[Active Players](./docs/Application_Tab_ActivePlayers.md) Current Players in the Instance. -## Action : KeyPressAction Definition +[Past Players](./docs/Application_Tab_PastPlayers.md) Players that have been in the instance since you started TailGrab for the last 15 minutes. -** Still Broken with Beta 3 release; Will be fixed in future release ** +[Prints](./docs/Application_Tab_Prints.md) Shows Prints that have been spawned into the instance by time/user id. -The KeyPress action will let you send keystrokes to a targed application by it's HWND Window Title, if the application runs windowless/without a title bar, this may not work for you. +[Emojis & Stickers](./docs/Application_Tab_Emojis_and_Stickers.md) Shows Emojis and Stickers that have been spawned into the instance by time/user id. -|Attribute | Definition | -|--------|--------| -| actionTypeValue | ```KeyPressAction``` See ```actionTypeValue``` | -| windowTitle | Windows application title; EG. ```VRChat``` | -| keys | An encoded defintion of keys to send to the application; see below | +[Config Tab, Avatars](./docs/Config_Avatars.md) Mark Avatars for Alerting and Blocking. +[Config Tab, Groups](./docs/Config_Groups.md) Mark Groups for Alerting and Blocking. -From https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.sendkeys?view=windowsdesktop-10.0 +[Config Tab, Users](./docs/Config_Users.md) See user activity you have encountered. -The plus sign (```+```), caret (```^```), percent sign (```%```), tilde (```~```), and parentheses ```()``` have special meanings to SendKeys. To specify one of these characters, enclose it within braces ```({})```. For example, to specify the plus sign, use "{+}". To specify brace characters, use ```"{{}"``` and ```"{}}"```. Brackets ```([ ])``` have no special meaning to SendKeys, but you must enclose them in braces. In other applications, brackets do have a special meaning that might be significant when dynamic data exchange (DDE) occurs. +[Config Tab, Line Handlers](./docs/Config_LineHandlers.md) Configure Actions to Trigger based on VRChat Log Events. -To specify characters that aren't displayed when you press a key, such as ```ENTER``` or ```TAB```, and keys that represent actions rather than characters, use the codes in the following table. +[Config Tab, Secrets](./docs/Config_Application.md) Configure API Keys and other application settings. -### Key Encoding -|Key Desired | Key Encoding | -|--------|--------| -|BACKSPACE | {BACKSPACE}, {BS}, or {BKSP} | -|BREAK | {BREAK} | -|CAPS LOCK | {CAPSLOCK} | -|DEL or DELETE | {DELETE} or {DEL} | -|DOWN ARROW|{DOWN}| -|END | {END} -|ENTER | {ENTER} or ~ -|ESC | {ESC} -|HELP | {HELP} -|HOME | {HOME} -|INS or INSERT | {INSERT} or {INS} -|LEFT ARROW | {LEFT} -|NUM LOCK | {NUMLOCK} -|PAGE DOWN | {PGDN} -|PAGE UP | {PGUP} -|PRINT SCREEN | {PRTSC} (reserved for future use) -|RIGHT ARROW | {RIGHT} -|SCROLL LOCK | {SCROLLLOCK} -|TAB | {TAB} -|UP ARROW | {UP} -|F1 | {F1} -|F2 | {F2} -|F3 | {F3} -|F4 | {F4} -|F5 | {F5} -|F6 | {F6} -|F7 | {F7} -|F8 | {F8} -|F9 | {F9} -|F10 | {F10} -|F11 | {F11} -|F12 | {F12} -|F13 | {F13} -|F14 | {F14} -|F15 | {F15} -|F16 | {F16} -|Keypad add | {ADD} -|Keypad subtract | {SUBTRACT} -|Keypad multiply | {MULTIPLY} -|Keypad divide | {DIVIDE} - -To specify keys combined with any combination of the SHIFT, CTRL, and ALT keys, precede the key code with one or more of the following codes. - -|Key Desired | Key Encoding | -|--------|--------| -|SHIFT | + | -|CTRL | ^ | -|ALT | % | - -To specify that any combination of SHIFT, CTRL, and ALT should be held down while several other keys are pressed, enclose the code for those keys in parentheses. For example, to specify to hold down SHIFT while E and C are pressed, use ```"+(EC)"```. To specify to hold down SHIFT while E is pressed, followed by C without SHIFT, use ```"+EC"```. - -To specify repeating keys, use the form ```{key number}```. You must put a space between key and number. For example, ```{LEFT 42}``` means press the LEFT ARROW key 42 times; ```{h 10}``` means press H 10 times. diff --git a/docs/Application_Tab_ActivePlayers.md b/docs/Application_Tab_ActivePlayers.md new file mode 100644 index 0000000..9fac523 --- /dev/null +++ b/docs/Application_Tab_ActivePlayers.md @@ -0,0 +1,30 @@ +[Back](../README.md) +# Active Player Tab + +The Active Player tab shows the players that are currently active in the game. It displays their name, userid, and current avatar, Pen Activity/Alerts, Instance Start (When you see them first in the instance), Copy Profile button and Report Profile button. + +[](./tailgrab_tab_active_players_elements.png) + +Below the Tab, the panel there is a search box that allows you to filter the list of active players by name. Enter a partial name and click 'Apply Filter' or click 'Clear Filter' + +Below that the list of active players in the instance. You can click on a player to view their profile and historical activity on the two bottom text boxes. The left box shows the player's profile and AI Evaluation, while the right box shows the historical avatar; emoji, sticker and print usage. The Column Header when clicked will sort the column; the default is "Instance Start" Decending. + +The _**Code**_ column indicates an alert state for the player. If there are no alerts, the column will be blank. Any player that has any alert will be highlighted yellow. + +| Code | Alert Type | +| --- | --- | +| A | Avatar Alert | +| B | Bio/Profile Alert | +| G | Group Alert | +| P | Print Alert | +| E | Emoji/Sticker Alert | + +The Copy Profile button allows you to copy the player's profile information to your clipboard, while the Report Profile button allows you to report the player for any inappropriate content using the VR Chat in-game reporting system. When you click the Report Profile button, it will open the a dialog mimicing the VR Chat reporting page as a model dialog with the player's information and reporting values pre-filled. + +Below the list of active players, there are two text boxes. The left box shows the player's profile and AI Evaluation, while the right box shows the historical avatar; emoji, sticker and print usage, these text boxes will update when you click on a player in the list above. If no player is selected, the left box will show "Select a player to view their profile and AI evaluation" and the right box will show "Select a player to view their historical avatar, emoji, sticker and print usage." These text boxes are horizontaly and verticaly resisable in the panel. + +The very bottom of the panel is the Status Bar, which shows the avatar processing queue size, how many players are waiting for an Ollama evaluation, the Instance Id and Elapsed time in this instance. + +[](./tailgrab_tab_active_player_report_profile.png) + +The Report Profile dialog allows you to report a player for any inappropriate content using the VR Chat in-game reporting system. When you click the Report Profile button, it will open the a dialog mimicing the VR Chat reporting page as a model dialog with the player's information and reporting values pre-filled. You can select the reason for reporting from the dropdown menu, and add any additional comments in the text box provided. Once you have filled out the necessary information or accept the AI evalutation, click the "Submit Report" button to send your report to VR Chat or "Cancel" to not send the report. diff --git a/docs/Application_Tab_Emojis_and_Stickers.md b/docs/Application_Tab_Emojis_and_Stickers.md new file mode 100644 index 0000000..7e790d1 --- /dev/null +++ b/docs/Application_Tab_Emojis_and_Stickers.md @@ -0,0 +1,31 @@ +[Back](../README.md) +# Emojis and Stickers Tab + +The Emojis and Stickers tab shows the player images that have been emoted into the instance. + +[](./tailgrab_tab_emoji_and_stickers.png) + +Below the Tab, the panel there is a search box that allows you to filter the list of players by name. Enter a partial name and click 'Apply Filter' or click 'Clear Filter' + +> [!NOTE] +> You can report a emoji or sticker if you have it's inventory id **inv_1234...** and user id **usr_1234...**. + +Next to the search elements is the "Report Inventory" button, which allows you to report any prior Emoji or Sticker for any inappropriate content using the VR Chat in-game reporting system. When you click the Report Print button, it will open the a dialog mimicing the VR Chat reporting page as a model dialog with the print's information and reporting values pre-filled. You can select the reason for reporting from the dropdown menu, and add any additional comments in the text box provided. Once you have filled out the necessary information or accept the AI evalutation, click the "Submit Report" button to send your report to VR Chat or "Cancel" to not send the report. + +Below that the list of Emojis and Sticker by players in or have been in the instance. + +Report Inventory button allows you to report the emoji or sticker for any inappropriate content using the VR Chat in-game reporting system. When you click the Report Print button, it will open the a dialog mimicing the VR Chat reporting page as a model dialog with the print's information and reporting values pre-filled. You can select the reason for reporting from the dropdown menu, and add any additional comments in the text box provided. Once you have filled out the necessary information or accept the AI evalutation, click the "Submit Report" button to send your report to VR Chat or "Cancel" to not send the report. + +[](./tailgrab_tab_emoji_and_stickers_reports.png) + +The Report Inventory dialog allows you to report a print through the ingame reporting API. + +- User Id: The unique identifier for the user who dropped the emoji or sticker, which is used to report the cdontent to VR Chat. Editable for user to input a user id they want to report. +- Inventory Id: The unique identifier for the inventory item, which is used to report the content to VR Chat. Editable for user to input a inventory id they want to report. +- Category: Either "Sticker" or "Emoji" based on the Inventory record. +- Report Reason: The reason for reporting the content, which can be selected from a dropdown menu of predefined reasons. +- Report Description: A text box where you can provide additional details about the reason for reporting the content or accept the AI evaluation of the print. +- Image Preview: A preview of the content image that is being reported, which can help you determine if the content is inappropriate and should be reported. + +- Submit Report Button: A button that allows you to submit the report to VR Chat once you have filled out the necessary information or accept the AI evaluation. +- Cancel Button: A button that allows you to cancel the report and not send it to VR Chat. \ No newline at end of file diff --git a/docs/Application_Tab_PastPlayers.md b/docs/Application_Tab_PastPlayers.md new file mode 100644 index 0000000..65e93c8 --- /dev/null +++ b/docs/Application_Tab_PastPlayers.md @@ -0,0 +1,12 @@ +[Back](../README.md) +# Past Player Tab + +The Past Player tab shows the players that have left the current instance. It displays their name, userid, and last avatar, Pen Activity/Alerts, Instance Start (When you see them first in the instance), Copy Profile button and Report Profile button. + +[](./tailgrab_tab_past_players_elements.png) + +Below the Tab, the panel there is a search box that allows you to filter the list of active players by name. Enter a partial name and click 'Apply Filter' or click 'Clear Filter' + +Below that the list of past players in the instance for the past 15 minutes. + +The Copy Profile button allows you to copy the player's profile information to your clipboard, while the Report Profile button allows you to report the player for any inappropriate content using the VR Chat in-game reporting system. When you click the Report Profile button, it will open the a dialog mimicing the VR Chat reporting page as a model dialog with the player's information and reporting values pre-filled. diff --git a/docs/Application_Tab_Prints.md b/docs/Application_Tab_Prints.md new file mode 100644 index 0000000..5362260 --- /dev/null +++ b/docs/Application_Tab_Prints.md @@ -0,0 +1,31 @@ +[Back](../README.md) +# Prints Tab + +The Prints tab shows the player prints that have been dropped into the instance. + +[](./tailgrab_tab_prints.png) + +Below the Tab, the panel there is a search box that allows you to filter the list of players by name. Enter a partial name and click 'Apply Filter' or click 'Clear Filter' + +> [!NOTE] +> You can report a prior print if you have it's print id **prnt_1234...**. + +Next to the search elements is the "Report Inventory" button, which allows you to report any prior print for any inappropriate content using the VR Chat in-game reporting system. When you click the Report Print button, it will open the a dialog mimicing the VR Chat reporting page as a model dialog with the print's information and reporting values pre-filled. You can select the reason for reporting from the dropdown menu, and add any additional comments in the text box provided. Once you have filled out the necessary information or accept the AI evalutation, click the "Submit Report" button to send your report to VR Chat or "Cancel" to not send the report. + +Below that the list of Prints by players in or have been in the instance. + +Report Print button allows you to report the print for any inappropriate content using the VR Chat in-game reporting system. When you click the Report Print button, it will open the a dialog mimicing the VR Chat reporting page as a model dialog with the print's information and reporting values pre-filled. You can select the reason for reporting from the dropdown menu, and add any additional comments in the text box provided. Once you have filled out the necessary information or accept the AI evalutation, click the "Submit Report" button to send your report to VR Chat or "Cancel" to not send the report. + +[](./tailgrab_tab_prints_report.png) + +The Report Print dialog allows you to report a print through the ingame reporting API. + +- Print Id: The unique identifier for the print, which is used to report the print to VR Chat. Editable for user to input a print id they want to report. +- User Id: The unique identifier for the user who dropped the print, which is used to report the print to VR Chat. +- Category: Always "print" for print reports. +- Report Reason: The reason for reporting the print, which can be selected from a dropdown menu of predefined reasons. +- Report Description: A text box where you can provide additional details about the reason for reporting the print or accept the AI evaluation of the print. +- Image Preview: A preview of the print image that is being reported, which can help you determine if the print is inappropriate and should be reported. + +- Submit Report Button: A button that allows you to submit the report to VR Chat once you have filled out the necessary information or accept the AI evaluation. +- Cancel Button: A button that allows you to cancel the report and not send it to VR Chat. \ No newline at end of file diff --git a/docs/Config_Application.md b/docs/Config_Application.md new file mode 100644 index 0000000..068421b --- /dev/null +++ b/docs/Config_Application.md @@ -0,0 +1,133 @@ +[Back](../README.md) +# Application Configuration + +The TailGrab application configuration panel is on the "Config" tab and then the "Secrets" sub-tab. All passwords and API keys protected, entering the configuration section hides the values so they cannot be coppied by shoulder surfers or screen recording software. The values are stored in the Windows Registry in an encrypted format and are loaded on application start. Hidden values are only written to the registry when there is a new value in the field and you click the "Save Secrets" button, so you can enter your credentials, save them, and then restart the application to get the services working properly. + +The TailGrab application will look for the following credentials to connect to your VRChat API and OLLama AI services from the Windows Registry in a encyrpted format. On the first Run you may receive a Popup Message to set the values on the Config -> Secrets Tab and restart the application to get the services running properly. + +[](./tailgrab_tab_configuration.png) + +## VR Chat API Credentials + +Tailgrab uses VR Chat's public API to get information about avatars for the BlackListed Database (SQLite Local DB) and to get user profile infoformation for Profile Evaluation with the AI services. + +The fields are your Web User Name and Password for VRChat, and the 2 Factor Authentication Key that is generation on account creation or enabling two factor authentication. + +**User** - This is your VRChat Username you use to log in to the VRChat website. + +**Password** - This is your VRChat Password you use to log in to the VRChat website. + +**2FA Key** - This is the 2 Factor Authentication Key that is generated when you set up 2 Factor Authentication on your VRChat account, this is used to generate the 2FA codes that are required to authenticate with the VRChat API. + +> [!IMPORTANT] +> VRChat's API is not officially supported for third party applications, and may change/break at any time; User credentials are stored in an encrypted format in the Windows Registry and used only to gather needed information about users in the instance you are in. +> This API is used to get Avatar Information, Profile Information and to Report User profiles and Stickers and Emojis to VRChat Moderation Team. The application does not perform any actions on your account that you do not explicitly trigger with a user action in the application, such as clicking a button to report a user or an image. + +### Getting your VR Chat 2 Factor Authentication key + +I certainly hope you are using LastPass Authenticator or Google Authenticator to manage your 2FA codes for VRChat. If you are not, please stop reading this and go set that up now to protect your Online Accounts. + +On LastPass Authenticator for the your VR Chat Entry, you can use the right Hamburger menu icon to get a dialog of options, one of which is to 'Edit Account', select that and you will see the 'Secret Key' field, copy the 'Secret Key' value to your clipboard and paste to something you can transfer to your PC (Or tediously type it in from the screen). + +## Ban On Sight (BOS) Management + +If you have a list of Avatars Ids and/or Group Ids that you want to be alerted to when encounted in the instance, you can have them in a shared team resource in the web, placing a publicly accessable URL in theses fields will have the application attempt to download the CSV files from the web and update the local database on startup. If the fields are blank nothing is downloaded and no changes are made. + +The format of the CSV files should be a single column with the header "Id" and then the Ids listed below, EG: +``` CSV +Id, Name +"avtr_12345678-90ab-cdef-1234-567890abcdef","Bad Avatar" +``` + +## Ollama Cloud AI API Credentials & Configuration + +OLLama Cloud AI services are used to evaluate user profiles for potential bad actors based on your custom prompt criteria. The OLLama API is called only once for a MD5 checksummed profile or Image to reduce API usage calls and cost. + +> [!IMPORTANT] +> Ollama Cloud API is free at this time! (2026/02) but may have usage limits any time; User credentials are stored in an encrypted format in the Windows Registry and used only to check on user content that has not been checked before for users in the instance you are in. Sign up at https://signin.ollama.com/ and see current pricing models https://ollama.com/pricing + +**Ollama Endpoint** - is the URL to your Ollama API instance, if you are using the cloud service, this should be ```https://ollama.com``` to use the cloud default, if you are using a local instance of Ollama, this should be ```http://localhost:11434``` or the URL of the host that Ollama is installed in. + +> [!NOTE] +> You can use a Localy installed Ollama instance with a API Endpoint of ```http://localhost:11434``` or the URL of the host that Ollama is installed in. You will still need to key an API Key, but it's contents do not matter. + +**Ollama API Key** - is the API Key generated from your Ollama Cloud account, this is used to authenticate your API calls to the Ollama Cloud service, if you are using a local instance of Ollama, this can be any value as the local instance does not require authentication. + +> [!NOTE] +> If the Ollama API Key is not set, no profile evaluation will be done and the application will not attempt to call the API, so you can use the profile evaluation features without setting up the API credentials if you want to just use it as a local database of good and bad actors. + +**Ollama Model Name** - This is the name of the model you have set up in your Ollama Cloud account that you want to use for profile evaluation, ```gemma3:27b``` has been selected to give a good balance of performance and cost, but you can use any model you have set up in your account. + +**Profile AI Prompt** - This is the custom prompt that you want to use for profile evaluation, you can use any prompt you want, but it should be designed to elicit semi formated response of + +``` + + +``` + +The default prompt is designed to look for potential sexual predators, but you can customize it to look for any criteria you want as so long as the reponse contains the classification on the first line and anything else on the rest. The Classifcations are based on VR Chat's Moderation Categories. + +|Classifcation | What it should look for | +|--------|--------| +| OK | Default value of nothing of interest in the profile. | +| Explicit Sexual | Any content that should not be in a PG13 instance | +| Harassment & Bullying | Any content that would be considered trolling based on Religion, Race or Sexual Orientation | +| Self Harm | Any content that could be considered a cry for help | + +> [!TIP] +> The Classification names are set, but how you word the prompt can get the AI to be as leinent or as strict as you want, you can also add more classifications if you want, just make sure to include them in the prompt and have the AI respond with the classification on the first line of the response for the application to parse it correctly. You can also use the reasoning section to give you more context on why the AI classified it a certain way, this can be helpful when you are on the fence about a user and want to make a judgement call on whether to report them or not. + +> [!NOTE] +> The system puts the Prompt plus the user profile information into the Generate API call, the full format is: +> ``` +> {UserAIPrompt} +> DisplayName: {displayName} +> DisplayName: {profile.DisplayName} +> StatusDesc: {profile.StatusDescription} +> Pronowns: {profile.Pronouns} +> ProfileBio: {profile.Bio} +> ``` + +> [!NOTE] +> The current prompt is defined as: +> From the following block of text, classify the contents into a single class from the following classes;\r\n'OK' - Where as all text content can be considered PG13;\r\n'Explicit Sexual' - Where as any of the text contained describes sexual acts or intent. Flagged words Bussy, Fagot, Dih;\r\n'Harassment & Bullying' - Where the text is describing acts of trolling or bullying users on Religion, Sexual Orientation or Race. Flagged words of base nigg* and variations of that spelling to hide racism.\r\n'Self Harm' - Any part of the text where it explicitly describes destructive behaviours.\r\nIf there is not enough information to determine the class, use a default of OK. When replying, return a single line for the Classification and a carriage return, then place the reasoning on subsequent lines, translate any foreign language to English: + +**Image AI Prompt** - This is the custom prompt that you want to evaluate images (Emoji & Stickers), you can use any prompt you want, but it should be designed to elicit semi formated response of + +``` + + +``` + +> _**Example Response for a NSFW Image:**_ +> ``` +> Sexual Content +> The image depicts anthropomorphic animals in a suggestive and sexually explicit situation with clear implications of sexual assault. This falls under the category of sexual content due to the nature of the depicted act and suggestive poses. +> ``` + +The default prompt is designed to look for potential PG13 violation, but you can customize it to look for any criteria you want as so long as the reponse contains the classification on the first line and anything else on the rest. The Classifcations are based on VR Chat's Moderation Categories. + +|Classifcation | What it should look for | +|--------|--------| +| OK | Default value of nothing of interest in the profile. | +| Explicit Sexual | Any content that should not be in a PG13 instance | +| Harassment & Bullying | Any content that would be considered trolling based on Religion, Race or Sexual Orientation | +| Self Harm | Any content that could be considered a cry for help | + +> [!TIP] + +> The Classification names are set, but how you word the prompt can get the AI to be as leinent or as strict as you want, you can also add more classifications if you want, just make sure to include them in the prompt and have the AI respond with the classification on the first line of the response for the application to parse it correctly. You can also use the reasoning section to give you more context on why the AI classified it a certain way, this can be helpful when you are on the fence about a user and want to make a judgement call on whether to report them or not. + +> [!NOTE] + +> The system puts the Prompt plus attached copy of the Image thumbnail for evaluation: + +## Alert Sounds + +Based on the three areas of interest, you can customize the alert sounds that are player. The first set are windows default alert sounds while the application will add any sound file (WAV, MP3 or OGG) that exist under the application installed directory under ```./sounds``` + +**Avatar Alert** - This sound is played when an Avatar is detected in the instance that is on the Ban On Sight list. + +**Group Alert** - This sound is played when a user in the instance is detected to be in a Group that is on the Ban On Sight list. + +**Profile Alert** - This sound is played when a user in the instance has a profile that is evaluated by the AI services to be of concern based on your custom prompt criteria. diff --git a/docs/Config_Avatars.md b/docs/Config_Avatars.md new file mode 100644 index 0000000..85c1d2e --- /dev/null +++ b/docs/Config_Avatars.md @@ -0,0 +1,22 @@ +[Back](../README.md) +# Config - Avatars + +The avatars panel lets you mark avatars for alerting. **At this time**, when you change the IsBOS value for an avatar, it will update the local database and trigger any alerting until you are in an instance with that **avatar name**. This current version will mark the avatar as **GLOBALY BLOCKED** for the VRC Account credentials you are using to prevent crashers from using the avatar, but it will not prevent other users in the instance from using the avatar. Use VRChat's Avatar web page to report bad avatars. + +[](./tailgrab_tab_config_avatars.png) + +> [!NOTE] +> We will be changing the Avatar Flagging system in the future to be more flexible and allow for more user directed flagging options, such as "Warn Only", "Crasher" and "NONE". + +You will notice a simular search and filter elements as the Active Players panel, this is to help you find the avatar in the local database by name or AvatarId. + +Next to the filter input and buttons is a field that accepts an avatar id, **avtr_1234...** and a button to "Add Avatar". This is for adding avatars that you have been alerted to by the Avatar Watch community, this will add the record then filter the list to the added avatar name. + +The list of avatars are shown below with the following columns: + +- Avatar Name: The name of the avatar as reported in the VRChat logs. +- Avatar ID: The unique identifier for the avatar, this is the value you can use to add an avatar to the list for alerting. +- IsBOS: This is the value that will trigger the alerting for the avatar, if this is set to true, then you will get alerts for users using this avatar in your instance. This is also the value that will mark the avatar as globally blocked for your VRC Account credentials, preventing crashers from using the avatar in your instance. +- Last Used: This is the last time this avatar was seen in your instance, this can help you identify if the avatar is currently being used by someone in your instance or if it was used in the past. +- Browser: This is a URL link to the avatar in the VRChat Avatar web page, this can help you quickly report the avatar if it is a bad avatar. + diff --git a/docs/Config_Groups.md b/docs/Config_Groups.md new file mode 100644 index 0000000..c08b278 --- /dev/null +++ b/docs/Config_Groups.md @@ -0,0 +1,22 @@ +[Back](../README.md) +# Config - Groups + +The groups panel lets you mark groups for alerting. **At this time**, when you change the IsBOS value for an group, it will update the local database and trigger any alerting until you are in an instance with a user that has that GroupId. + +[](./tailgrab_tab_config_groups.png) + +> [!NOTE] +> We will be changing the Group Flagging system in the future to be more flexible and allow for more user directed flagging options, such as "Warn Only", "Crasher" and "NONE". + +You will notice a simular search and filter elements as the Active Players panel, this is to help you find the group in the local database by name or GroupId. + +Next to the filter input and buttons is a field that accepts an group id, **grp_1234...** and a button to "Add Group". This is for adding groups that you have been alerted to by the Group Watch community, this will add the record then filter the list to the added group name. + +The list of groups are shown below with the following columns: + +- Group Name: The name of the group as reported in the VRChat logs. +- Group ID: The unique identifier for the group, this is the value you can use to add an group to the list for alerting. +- IsBOS: This is the value that will trigger the alerting for the group, if this is set to true, then you will get alerts for users having this group in your instance. +- Last Used: This is the last time this group was added/updated by you. +- Browser: This is a URL link to the group in the VRChat Group web page, this can help you quickly report the Group if it is a bad actor. + diff --git a/docs/Config_LineHandlers.md b/docs/Config_LineHandlers.md new file mode 100644 index 0000000..a5fe76d --- /dev/null +++ b/docs/Config_LineHandlers.md @@ -0,0 +1,135 @@ +[Back](../README.md) +# Application Log line parsing and actions - "./Config.json" File + +The confiuration for TailGrab uses a JSON formated payload of the base attribute "lineHandlers" that contains a array of LineHandler Objects, Those may have a attribute of "actions" that contain an array of Action Objects. This configuration is loaded on application start. + +## LineHandler Definition + +The LineHandler defines what type of system action to perform, what regular expression to use to detect that type of log line and user actions to perform when detected. + +|Attribute | Definition | +|--------|--------| +| handlerTypeValue | An enumeration value of the internal LineHandler code segments. See ```handlerTypes``` | +| enabled | Boolean ```true``` or ```false``` to direct the application to include or temporarly ignore the configuration. | +| patternTypeValue | An enumeration value of ```default``` or ```override```; Default will use the programmer's defined default for the Regular Expression to match/extract and a Override will allow the user to fine tune or respond to VRChat application log changes with the attribute ```pattern``` | +| pattern | The Regular expression for the Pattern to match/extract, does nothing unless patternTypeValue is set to override | +| logOutput | Boolean ```true``` or ```false``` to direct the application to log the output of the Line Handler. | +| logOutputColor | A value of ```Default``` will use the programmers ANSI codes for the log output, if you use the last digits of the ANSI codes here, they are used. EG ```"37m"``` | +| actions | A array of Action Configuration elements or do nothing by leaving it as an empty array ```[]``` | + +### actionTypeValue Enum Values + +|actionTypeValue | Definition | +|--------|--------| +| DelayAction | Delay a defined amount of time before next action. | +| OSCAction | Send OSC Avatar Parameter values to your VRChat Avatar. | +| KeyPressAction | Send Keystrokes to a named open window title on your system. | + + +### Action: DelayAction Definition + +The Delay Action will allow you to pause other actions with millisecond precision. If you need to pause for 1 second, use 1000 as the delay time. This action is used when there is a need for a sound trigger to play or you want to send stacked keystrokes to an application that is running. + +|Attribute | Definition | +|--------|--------| +| actionTypeValue | An enumeration value of the internal LineHandler code segments. See ```actionTypeValue``` | +| milliseconds | integer value of milliseconds to wait for. | + +### Action: OSCAction Definition + +The OSC Action will allow you to send values (```Float```/```Int```/```Bool```) to your VRChat avatar that could be used to trigger animations on it during a action set. + +|Attribute | Definition | +|--------|--------| +| actionTypeValue | ```OSCAction``` See ```actionTypeValue``` | +| parameterName | The VRChat Avatar Parameter Path to send to; EG. ```/avatar/parameters/Ear/Right_Angle``` | +| oscValueType | OSC Value types associated with that Parameter Path; ```Float``` or ```Int``` or ```Bool``` | +| value | The Value to send to your avatar; Floats expect a decimal place ```0.0```, Int expect no decimal place ```0```, and Bool expects either ```true``` or ```false``` + +### Action: TTSAction Definition (Not Working ATM) + +The TTS Action will allow you to say a phrase when triggered. + +|Attribute | Definition | +|--------|--------| +| actionTypeValue | ```TTSAction``` See ```actionTypeValue``` | +| text | The phrase you wish to have spoken | +| volume | Volume 0...100 | +| rate | The speed of the speech -10...10 | + +### KeyPressAction Definition in Actions + +** Still Broken with Beta 3 release; Will be fixed in future release ** + +The KeyPress action will let you send keystrokes to a targed application by it's HWND Window Title, if the application runs windowless/without a title bar, this may not work for you. + +|Attribute | Definition | +|--------|--------| +| actionTypeValue | ```KeyPressAction``` See ```actionTypeValue``` | +| windowTitle | Windows application title; EG. ```VRChat``` | +| keys | An encoded defintion of keys to send to the application; see below | + + +From https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.sendkeys?view=windowsdesktop-10.0 + +The plus sign (```+```), caret (```^```), percent sign (```%```), tilde (```~```), and parentheses ```()``` have special meanings to SendKeys. To specify one of these characters, enclose it within braces ```({})```. For example, to specify the plus sign, use "{+}". To specify brace characters, use ```"{{}"``` and ```"{}}"```. Brackets ```([ ])``` have no special meaning to SendKeys, but you must enclose them in braces. In other applications, brackets do have a special meaning that might be significant when dynamic data exchange (DDE) occurs. + +To specify characters that aren't displayed when you press a key, such as ```ENTER``` or ```TAB```, and keys that represent actions rather than characters, use the codes in the following table. + +#### Key Encoding +|Key Desired | Key Encoding | +|--------|--------| +|BACKSPACE | {BACKSPACE}, {BS}, or {BKSP} | +|BREAK | {BREAK} | +|CAPS LOCK | {CAPSLOCK} | +|DEL or DELETE | {DELETE} or {DEL} | +|DOWN ARROW|{DOWN}| +|END | {END} +|ENTER | {ENTER} or ~ +|ESC | {ESC} +|HELP | {HELP} +|HOME | {HOME} +|INS or INSERT | {INSERT} or {INS} +|LEFT ARROW | {LEFT} +|NUM LOCK | {NUMLOCK} +|PAGE DOWN | {PGDN} +|PAGE UP | {PGUP} +|PRINT SCREEN | {PRTSC} (reserved for future use) +|RIGHT ARROW | {RIGHT} +|SCROLL LOCK | {SCROLLLOCK} +|TAB | {TAB} +|UP ARROW | {UP} +|F1 | {F1} +|F2 | {F2} +|F3 | {F3} +|F4 | {F4} +|F5 | {F5} +|F6 | {F6} +|F7 | {F7} +|F8 | {F8} +|F9 | {F9} +|F10 | {F10} +|F11 | {F11} +|F12 | {F12} +|F13 | {F13} +|F14 | {F14} +|F15 | {F15} +|F16 | {F16} +|Keypad add | {ADD} +|Keypad subtract | {SUBTRACT} +|Keypad multiply | {MULTIPLY} +|Keypad divide | {DIVIDE} + +To specify keys combined with any combination of the SHIFT, CTRL, and ALT keys, precede the key code with one or more of the following codes. + +|Key Desired | Key Encoding | +|--------|--------| +|SHIFT | + | +|CTRL | ^ | +|ALT | % | + +To specify that any combination of SHIFT, CTRL, and ALT should be held down while several other keys are pressed, enclose the code for those keys in parentheses. For example, to specify to hold down SHIFT while E and C are pressed, use ```"+(EC)"```. To specify to hold down SHIFT while E is pressed, followed by C without SHIFT, use ```"+EC"```. + +To specify repeating keys, use the form ```{key number}```. You must put a space between key and number. For example, ```{LEFT 42}``` means press the LEFT ARROW key 42 times; ```{h 10}``` means press H 10 times. + + diff --git a/docs/Config_Users.md b/docs/Config_Users.md new file mode 100644 index 0000000..7a3e170 --- /dev/null +++ b/docs/Config_Users.md @@ -0,0 +1,17 @@ +[Back](../README.md) +# Config - Users + +The Users panel lets you search users you have encountered. + +[](./tailgrab_tab_config_users.png) + +You will notice a simular search and filter elements as the Active Players panel, this is to help you find the user in the local database by name or UserId. + +The list of users are shown below with the following columns: + +- User Name: The name of the user as reported in the VRChat logs. +- User ID: The unique identifier for the user. +- Elapsed Minutes you personaly have seen the user with you. +- Last Used: This is the last time this group was added/updated/seen. +- Browser: This is a URL link to the group in the VRChat User web page, this can help you quickly report the User if it is a bad actor. + diff --git a/docs/amplitude.cache.json b/docs/amplitude.cache.json deleted file mode 100644 index d9f1e50..0000000 --- a/docs/amplitude.cache.json +++ /dev/null @@ -1,1163 +0,0 @@ -[ - { - "app_version": "2025.4.2p1-1769-54ede42845-Release", - "device_id": "ffffffffffffffffffffffffffffffffffffffff", - "device_model": "B650 GAMING X AX V2 (Gigabyte Technology Co., Ltd.)", - "device_name": "computer", - "event_id": 17, - "event_properties": { - "_lastWorldNumberOfCustomEmojiSeen": 24, - "_lastWorldNumberOfCustomEmojiSent": 0, - "_lastWorldNumberOfDefaultEmojiSeen": 19, - "_lastWorldNumberOfDefaultEmojiSent": 0, - "_lastWorldNumberOfEmojiSeen": 43, - "_lastWorldNumberOfEmojiSent": 0, - "_lastWorldNumberOfExclusiveEmojiSeen": 0, - "_lastWorldNumberOfExclusiveEmojiSent": 0, - "_lastWorldNumberOfPremiumEmojiSeen": 0, - "_lastWorldNumberOfPremiumEmojiSent": 0, - "_lastWorldRadialEmojiMenuOpenCount": 0, - "_lastWorldRadialItemMenuOpenCount": 0, - "_lastWorldRadialMenuOpenCount": 5, - "_lastWorldRadialStickerMenuOpenCount": 0, - "avatarIdsEncountered": [ "avtr_ffffde02-6f77-4336-a494-f6bf94709ed6", "avtr_3735a4c4-7f49-411a-9836-b58cfa4cbd8c", "avtr_b76cdcba-2dfb-4729-bfb0-d271a7f36618", "avtr_c45f79db-39cc-4800-aba9-0b2cc976032d", "avtr_f3d43523-8b73-4eb3-8a1e-a18a976d8402", "avtr_f3a93245-fed1-4924-a3f8-cd2fee4de468", "avtr_fbd78a46-9f94-4a39-a2fb-8f60bad72d93", "avtr_21ba43e3-3f1d-4e1f-98e5-6eeeb1869adf", "avtr_2eac743e-28f1-41ec-8860-b76c18c0a32e", "avtr_9f590304-4fb8-4bfa-908b-4c0a463bfcbe", "avtr_c0e689fc-e12b-488f-a4c1-25364acbcd27", "avtr_4eac635e-c7a5-4ca2-8c7d-6446c940cfb7", "avtr_b3531b29-0f2d-4e9a-92b8-55c010225adf", "avtr_87aa8946-b086-440f-bcda-c4fddb03ae8d", "avtr_80f96da2-97c6-4d63-86df-60d22d0773be", "avtr_8e21c1f1-cbb5-4eb9-9d10-2114ce7182fd", "avtr_34f4a087-469f-4f6f-ac54-3478b1791e99", "avtr_29bedb9e-0573-4864-a922-2deb982df001", "avtr_47a621f5-fe87-4719-8010-4d77f955688f", "avtr_254ab9e5-b24f-4921-9b0d-b33cddb0e0eb", "avtr_e4f4c5fe-cb84-4b61-8ee7-c35f89a73495", "avtr_5972519b-a8ba-434d-80b5-cc2d52490a06", "avtr_6734f450-85be-482e-bf0c-07080fa76b92", "avtr_461be854-ac29-4c41-81d0-98a58a27f68c", "avtr_10b5f9dd-a207-4578-bae0-06daadfab5d8", "avtr_158ccd1c-32b6-49a8-9034-71e915ea507b", "avtr_9087a9b4-8b9b-435e-9819-8bf3c37b441f", "avtr_e510c545-0069-483b-8aea-cb4d5bc1d963", "avtr_8d487c0a-8de4-424d-a79d-605f82843701", "avtr_26a7bc04-3bd6-400e-a07b-c2f98f43779d", "avtr_4012e496-5b30-4388-a6e3-3e039e950ce5", "avtr_6d00f7af-7b2e-467a-8e83-0a7339dc1189", "avtr_8a9e981e-fb71-45de-a7bd-9ef058d3e90a", "avtr_0e84c5f0-3501-4ba6-869a-ca5a9095a979", "avtr_18097f12-334b-4685-b12b-b99337593333", "avtr_e507e9fa-5cae-4da7-b468-a0d612ed8c38", "avtr_a46dd884-ffb0-46b2-909b-146009aab1b3", "avtr_b8b2b0fb-02e6-4ded-b10e-4969b8d881fa", "avtr_5a0c3fab-cdfb-4ada-903e-384008f19d46", "avtr_78313a05-3ab3-4397-975a-e877ca76fa71", "avtr_8fa2bf67-380a-40c1-a3d6-c782afce65bc", "avtr_8991766d-c760-4871-9a4d-b45a58e8ae17", "avtr_709e10b6-a01c-405d-8360-b6420af33839", "avtr_99dcb798-88ac-45ec-878c-93f5dc178b44", "avtr_ac6486b8-5a50-4c48-b4c3-4897ff0ce755", "avtr_98311159-19c4-47ab-a0f5-7080ed3efddf", "avtr_301cd5c5-8f08-427f-9325-51344ec35fc9", "avtr_2ee35118-45b1-43b5-a321-b66457a7161e", "avtr_d1c73bba-dc14-47be-9aa9-615176a7aee5", "avtr_4d9670d5-6fc7-4d50-bd7b-8f4b374eac3f", "avtr_251b8a7a-e980-4765-84ad-36242913bc86", "avtr_e02b9561-7f89-4d0a-aa84-d7126c9869d6", "avtr_91f2b676-301c-4d4f-87e1-8f41e4997c10", "avtr_dc1a37a8-ae05-4123-bb13-5b6707fe735d", "avtr_a83f076b-e103-49c8-87df-35b63eaf5597", "avtr_272bf824-eb1a-4cbd-b621-fdd5d5fc0b26", "avtr_971bbe58-b1e4-4e18-9183-7889823ba21e", "avtr_d4663bac-d4ed-4ae1-beb8-5bd03cbfcf97", "avtr_dfa6c16c-7175-43e0-89d5-3587f45982e8", "avtr_175b4670-1d43-48db-b473-f31015990bde", "avtr_327b96e6-1a2c-4cc3-9834-e8e705d126e2", "avtr_b8a2befc-f618-4b56-bbb1-c17607d8782c", "avtr_56350c44-b448-4f7d-b3cd-785dbf653e9e", "avtr_02ee2c2d-066f-4419-ae2f-c3f222dd2b3b", "avtr_f13ec029-537b-4188-ab28-3a7a408ddba5", "avtr_64690f7d-54ac-477c-8fc7-4c092274c2e1", "avtr_f528ae3f-6881-40cf-9d49-0676ab6c0ef6", "avtr_b0688ab7-61fc-466b-a3e6-a311c8c5fd51", "avtr_713992bb-4b79-4e7f-ab57-41e887044675", "avtr_f0da3384-9565-44d1-ad84-a642f7e12840", "avtr_646b53c7-8e03-4fdb-a84d-ee566285ab41", "avtr_70d6f7a3-134b-4917-82c9-b11bd30ae01f", "avtr_66a38f43-7a4b-4773-a9f8-d4922bf67c48", "avtr_fcb1c99a-d36e-4774-8524-477140d99206", "avtr_336d4fde-eb09-4f14-a170-2c887d4c0671", "avtr_59d7a03a-6db2-44a6-b65e-a76ef95925c5", "avtr_27717baa-b907-49e2-92f0-aab5f49759b0", "avtr_eff840b2-381f-4006-8c56-258bc1530475", "avtr_08ef3d72-dec0-4abb-b732-fb1e908d4c7c", "avtr_c628077a-c5de-4bc5-9b45-072dadf5d779", "avtr_7dd285b9-d47d-49fb-9171-8af6b6014239", "avtr_22c3c676-6d39-4191-8729-88df010b3d24", "avtr_34d396ef-2da6-4808-9db7-c7d6b1ae641d", "avtr_a91bb257-ca08-43fc-b32c-2fc52927c931", "avtr_c91f4d9e-07b6-4b81-b7e7-8b94075c33f0", "avtr_70f009aa-66e5-4bd6-8326-d0ef3615f136", "avtr_dabcfd12-31c9-43fd-964e-562179dfcf52", "avtr_447a7c31-2566-48f7-b2dc-9bbbe23f33af", "avtr_7be3cca2-d3b5-4987-9e36-aac2a05999ca", "avtr_25be7dec-e41f-499b-873f-c01d0739f981", "avtr_dde1d24e-1294-4d84-8c60-ead5fbca6322", "avtr_1c371197-80fe-494c-8a9f-3ccdf2869c78", "avtr_24c8fa0b-9483-42a5-8bf5-1f3d1ab6c860", "avtr_7721fd9f-97d0-4691-bca6-68ab4bc1c911", "avtr_cf3f2a0e-b5ed-468b-b233-6b75098914f6", "avtr_d2c0a7b4-7b9d-4997-a404-6f2605dd9ada", "avtr_9c3d471c-74b5-43b7-9a06-36688d073ac8", "avtr_003e2f3c-fbf5-4fbb-8817-d610ffac7d1a", "avtr_adb918bb-e1ab-43ef-9dd7-deba480be161", "avtr_326a39cd-1879-4966-a068-405e3b8a8149", "avtr_dfd5d941-660e-456b-af18-e56f59aea011", "avtr_855798ef-09a8-4109-90b3-e135a3271d88", "avtr_c3fafb98-5a8e-4ef0-8c25-2d56d80a3b9c", "avtr_a48ef3e7-a2d4-462d-ac24-436343fa660f", "avtr_9c8bdca3-fea7-4b35-a8a7-9facee296fcf", "avtr_5f597b27-b984-4585-a97d-a71921ceaf17", "avtr_85243051-8aa4-4003-98c3-c2fdcaa6abef", "avtr_32f4bcdd-4a13-48d5-b8b9-54fa8dfaa6d2", "avtr_15d8172c-a59b-4adb-a93e-75f36582c9ee", "avtr_7bd9f1c7-b090-4681-9a30-4ae690dfd34e", "avtr_660b7277-5eb4-48b0-8dd4-1ccc3f966750", "avtr_1bf185c3-f00e-4dd0-8b13-abeaa4d6ca4a", "avtr_c957b600-7893-432a-b06d-0f328135bbdd", "avtr_a7903484-3ec6-44dc-9974-a254802b8ac8", "avtr_5dca5fc7-75d8-4477-a15e-6d8c16748104", "avtr_e0604303-6dd6-4bda-aa95-f2551cc305ad", "avtr_e8a0c212-0e03-4233-9a2e-cf3aeeec7a77", "avtr_889147db-d613-4636-abf2-f788a73f8a53", "avtr_b004eeec-cf1d-44a1-a6e3-0cdc1fbc31ed", "avtr_1fd00093-3161-46a3-9683-95e39a9c42a6", "avtr_b2833054-a115-4bb5-ad57-7fe4d496070b", "avtr_d474fbff-1cfa-47c0-be52-cdf688a936d4", "avtr_f0c08c64-2a77-4571-aa2b-803f90d473db", "avtr_f04facd5-a2ce-49c6-9756-e963fe684346", "avtr_31184187-7d31-4e33-a564-5d6ce80a165d", "avtr_d9b4bba0-12bc-4301-8c49-e14d1e838f78", "avtr_d3ddd188-ca1e-47d7-a731-5c76025880c1", "avtr_8639080a-b1dd-46b0-bb04-cf6b70f732b2", "avtr_7db36341-f2b4-4aab-b0b0-d175f0af8813", "avtr_52a4ab88-a640-4d07-adbb-ac9fa74051d7", "avtr_21d6bcce-b0c8-40b3-abe6-abba51bd0588", "avtr_8385a57a-af69-4193-9db1-fc42f5a5ef6e", "avtr_4a20bfcb-5d86-4402-9db4-326c8547e939", "avtr_5d972753-d594-416b-ac86-1d1c007de112", "avtr_faec9bd9-24c7-4c77-914e-e0a009447e13", "avtr_40e50d81-7db3-4ea4-825d-e184493c031a", "avtr_f86b5358-aa71-4b86-b474-06daf9258580", "avtr_fa9283f4-03d2-4b8b-acf5-12ad08cf802a", "avtr_f4d501b5-5e4e-4564-9ba3-d0d677ad9215", "avtr_2783ce74-6d9b-40d1-8b89-43f4f1bda087", "avtr_c9687ab1-8d84-45dc-aaf5-67fefd44ed4e", "avtr_de7b31b3-cd70-47ee-a5df-47de9935e279", "avtr_802e5d6e-24d5-4ef4-a9ef-6ac14967c24d", "avtr_3d7f1ae3-1bdd-4bae-ab6f-52bfdea26dc6", "avtr_83d6d512-09c5-45a3-aee8-8746dbff9c60", "avtr_6e34756b-f8d7-4574-9576-d1c4b5a98db0", "avtr_b6bdcc47-56eb-4137-8de1-90b04366ea91", "avtr_341f6407-0b4e-4527-b9c2-e70fa9372895", "avtr_500b2c0b-4ff5-418b-b0be-484b58f5c623", "avtr_c4961195-1980-4a98-bb95-3cbe0e063463", "avtr_7ff13960-dd4a-418b-bb03-9704545ff248", "avtr_105db449-da5d-4353-8f72-8c4aa3d4bece", "avtr_f00e810f-3232-4d83-a842-f214087a2517", "avtr_f1a78dd4-8768-403e-8d7e-747c4a3e499c", "avtr_4ab937fa-c2f7-4bd6-9be0-89126b9fcbb3", "avtr_d58af7b9-f4da-4184-bb14-d7c8b5b24013", "avtr_14631b38-e1ed-44c3-b2c6-0d8d78b8f195", "avtr_feca09da-b781-4736-a244-f8f3fa4b5364", "avtr_a0b3d528-d443-482f-b1ef-7a59c1f246a1", "avtr_6aa13b3a-44a8-49e8-9680-1d3b03010878", "avtr_1f9d0e0e-dfd7-4abb-a41b-b80324aa1199", "avtr_99efc003-10e6-48fc-be6a-3a11696cccd7", "avtr_a16027c1-6d6e-4709-9cb2-fdf01cd82fdf", "avtr_799d0ddb-a007-403d-8eb6-51758b4a9ac4", "avtr_e6c4962b-7642-499a-ae25-84be4c17be1e", "avtr_cbddf575-a96e-4819-b171-814d42ab9bfb", "avtr_900fa40b-cbcb-411b-9201-f43849b184cf", "avtr_c9eda0fb-8a3c-4e09-831b-17b6a48d0504", "avtr_03f1f1be-8df5-4128-842f-9ac0111a2f8f", "avtr_9c6c641d-4a66-4548-9766-165db77fa9b5", "avtr_0d3d12b5-2136-41d9-bb57-2f56ff2ca191", "avtr_93739ec4-c85e-4105-bfd1-807cb0bf9b26", "avtr_56145b8a-2ab7-4cb6-ad37-6601a766bf87", "avtr_ce8fc2e9-3877-4000-8c72-a89676c6fdc3", "avtr_0fe43873-4a4e-464d-84da-5d4bfa0f4ca9", "avtr_763aad1e-0b66-42e2-b2d0-3052be42b489", "avtr_8ee2b82e-1ecc-490e-a67a-ec91aaa12c9d", "avtr_07917a03-5e0b-48e9-b041-e94086bc658f", "avtr_dc831a53-8e3b-498d-8e26-c9c84cff44f7", "avtr_b4afcc9b-52e4-46c6-b7e7-4961207030b3", "avtr_d6b65af7-fafb-4e21-9ef4-a6b86721b6d0", "avtr_e9d3469a-1251-4def-8079-4efc48ad12d6", "avtr_e169f641-c9cb-4ab2-8b2a-bb1884492796", "avtr_eb042a60-60c8-48d7-93ac-f4a762800f06", "avtr_13ce23f5-1bdc-4162-8375-b261411edac6", "avtr_effa03e8-a463-4d28-b1e4-f5cba2b1e477", "avtr_6f1e8cc9-1c12-490f-8a18-55e6e57e5ad7", "avtr_e283f092-0300-4014-955a-1bc3eb9e5e4a", "avtr_c494c36e-a93e-42f9-8372-925c852a21cb", "avtr_fbbfc65b-db5e-4e16-9d49-ed50757cb960", "avtr_e0e34d6b-e771-4e04-b6b2-1cd02756722d", "avtr_c04c65fa-305a-4a40-a8a8-f7475d1a9f8f", "avtr_a16d16fa-971e-4286-a349-c9afe5aecf4c", "avtr_87e7965e-dcfd-4c9e-ac69-d4c2251bf04a", "avtr_fdfd83f7-5c2e-4523-913c-59fb79bd2981", "avtr_17a86159-0485-410b-a963-df1b5a140f1a", "avtr_b4fe0d5a-9402-4b65-b542-ad395309d690", "avtr_2d830d70-86a6-474a-a953-a2ced48c030c", "avtr_c800daf4-c936-41fb-8a28-4052e6e3ddbe", "avtr_d65bfbe4-7ac3-4aee-8d24-aabf0b151ce9", "avtr_cefb9dff-5b04-4012-99c6-fc6c2b1a0eb3", "avtr_86370ee7-adce-45f4-9d18-d9ea86be80fc", "avtr_845c5be2-1f45-4cc1-acad-4aba30fc6828", "avtr_baaaa18e-a6d0-4ae9-8061-5dd927666e91", "avtr_ee7f49fe-e3aa-4e7a-8570-fa6a1c549146", "avtr_89f88e0c-95a8-4c90-a089-47f8ab16c55e", "avtr_27d41f72-db1a-43b5-b19b-70afe5b29b97", "avtr_4693d8dc-294b-4273-a4e0-b229bb45b70f", "avtr_f6f0b1d5-56f9-4a50-a3c9-6f5bac0c2e7c", "avtr_3e5c1ed6-30df-44e1-89d4-2638517bd137", "avtr_4788985f-1de0-4be1-87cd-430ab91213e6", "avtr_a6dd7bcf-8f91-48b6-8ac4-9a47b19128a3", "avtr_5e1b6c2f-11ed-4c0c-a4fb-432a8754d5ed", "avtr_187c6304-58cd-4ef5-a3e9-9e10da0fc082", "avtr_cb9cfcbb-0cc2-4bd2-a538-8267211d63b4", "avtr_bb3e9fa9-aaab-4416-8525-b996f30b9e7f", "avtr_353e04cc-d5e4-4292-8a05-4b034c9dfe9e", "avtr_720b749b-3f6d-4def-b2cb-26f5a9bf622e", "avtr_2ca9feb7-27e1-4bc7-a846-2c726538e38b", "avtr_e31ac7b6-fa04-4c9f-83dc-61d5030d4118", "avtr_55f79fe3-77c9-4d2d-8ebe-e1f530d5ff9d", "avtr_5304771b-135a-4cf5-85ff-a5874bea0fe6", "avtr_f1dc9829-5452-426b-b3c3-6e011df70a73", "avtr_1579c4b6-d647-4ded-a5c2-b78b9c6ea686", "avtr_4de6b2e6-ef19-41f1-bd14-d3e447791e46", "avtr_7e6705f6-0d98-4f91-abfa-0a40034ba91d", "avtr_89deb764-fabf-487c-a600-e360c0ea5ebf", "avtr_0186b002-c49e-437d-a894-20c92d3b5271", "avtr_db71f482-1f09-4d40-a466-737e981d0636", "avtr_ebb09c9c-1764-45ce-ad57-f958ed45daf7", "avtr_e5c44233-5b0a-4b14-b79a-f832117be6c8", "avtr_bc1c99ca-4e7a-4fde-b799-881d828aa331", "avtr_fe40ae05-6c8e-47ce-870d-90811f5ac638", "avtr_0c96db4c-a4fb-43e8-9f1c-8fc43421cf24", "avtr_49c91647-ddcf-4aaf-92cf-fc15df43b872", "avtr_4bb651f6-4cb7-4b33-8d98-68c8536f1978", "avtr_8f119838-724c-4cee-b9d2-3fe06afd971a", "avtr_740d6530-2d93-45c7-9d2b-2c916e98f5aa", "avtr_0d01a77c-8f34-4c77-ac03-965e7bd263e4", "avtr_ac4b48ed-62b5-42d4-b362-a25f96d247a9", "avtr_959651f9-9c4e-42c7-a81b-f6d6c4f4b851", "avtr_b941d49a-5a13-4322-85cc-e75f5f7ce25e", "avtr_2189cdc2-0095-4a52-8c15-2e7afd152551", "avtr_ab220cb5-89a8-4238-884c-1793741eee41", "avtr_2a54ebc2-c047-4522-bf30-17777a48badc", "avtr_f03cff37-85f1-40e9-81e8-87ce8ea27494", "avtr_ef086c0f-29ea-4150-8a71-c42adc2d0262", "avtr_2be2b2d5-bec4-4b07-9bf8-8bb81606c78d", "avtr_ece4743a-878f-471d-967d-c0f112401d4a", "avtr_2622c612-612c-44c1-bbf2-d9707e535b81", "avtr_4422a957-2a83-40e9-a86a-3da4f0c86dda", "avtr_be3f1722-4f0a-4d85-8d1a-69bfa8f12d5e", "avtr_b3d7d7d0-f247-4c4a-989c-5d13d10916a5", "avtr_f3c19a29-1e8a-41ad-a1b3-a488bfa914d0", "avtr_3262d843-8711-4b84-a5e1-9a34428330b0", "avtr_aae7cf6e-fca8-4ab6-a59c-1dffc87c16a0", "avtr_dc06420b-4d9b-4ee0-8a6a-01388e4b03fc", "avtr_418992c8-570b-422d-b880-94acd47fbd4c", "avtr_2b1c6a4f-6633-4a1c-b5e8-ad9b81143ea6", "avtr_124c1225-274c-40f7-95fe-4f396e5b6a55", "avtr_ba7cde83-578a-435a-b7a7-192728dcc83b", "avtr_cc9b4bd9-7f9a-4a40-9367-debd746aecf9", "avtr_770ebb8e-9bcb-42f0-b6f2-b8e36bbfdd23", "avtr_a9bdf9b4-36e1-40ea-aed4-9ad90a04cdea", "avtr_a028e6e4-8e7d-4c17-8049-da33d4838547", "avtr_61683095-5762-4a2e-8c81-3be38e518b9f", "avtr_d52ba3bb-2776-4dae-8fd3-2497f01cf27b", "avtr_ee5bcd48-2950-46ad-a86b-004b74094966", "avtr_e8332014-3301-4747-922e-e3353509c23d", "avtr_2a58e8d6-359f-4174-a131-6e278e019e33", "avtr_848fd5a4-4fcf-459c-b660-5e9fbdb291d8", "avtr_dd8ea581-15fc-4a3a-9711-66fa86eb77e3", "avtr_e0386a00-68a5-4b13-a775-ee63393b0d41", "avtr_69de05f8-9939-40d7-a167-96f5c4d39f58", "avtr_fc5fab37-8e91-4796-9678-7597126503eb", "avtr_8f60d715-7e9e-4bac-92b2-70877432d677", "avtr_250309e1-2608-44c7-a77b-2097af868f4a", "avtr_e61a4bd8-fdf0-482f-b801-bd6fa86a936e", "avtr_c018b006-7080-49c5-8603-5f90b17b1be6", "avtr_91afb9aa-98e9-4cca-a3ad-39e3513dda6c", "avtr_709d42d8-2723-4573-acd0-fdb9a877c571", "avtr_13baa8df-af6a-46fb-8d77-b790112a6b71", "avtr_a5192334-f6a8-464e-8852-a784d4baa548", "avtr_b13db159-a105-4c21-9691-0de17b962feb", "avtr_c276a465-5fb6-4cdd-b827-e2b89c01c9f6", "avtr_00530e67-4faf-40a2-a647-9ce9f493a339", "avtr_19f53984-a917-4c88-a5dd-b6b7ae808721", "avtr_d509d8c3-3c87-466e-8cb9-81261e6a0bff", "avtr_2c42a82e-e21b-4d5e-9419-c19f1045f75f", "avtr_d5d944da-d48e-480c-8009-e2731ff5fb26", "avtr_ef6650cd-7467-4d06-8358-5ba23871daaf", "avtr_566d1908-e02b-4be7-9fe6-aab9a2fa64e1", "avtr_44e267b4-14fe-4325-9c24-92a8b7458f80", "avtr_19f4829a-9dbd-4012-bfb5-975ae2036ded", "avtr_83032bab-efb2-4a2c-ba61-b8e840d7926d", "avtr_1a6c8505-43e3-4ea8-b9d4-462d46cd3330", "avtr_bdc49e85-7fd5-4d70-affc-b06c4e2e0fdb", "avtr_14bbd680-081a-4ce9-8d3e-331bf6ba1bc6", "avtr_8226155a-ec40-4c19-bce4-80d1b62fbcf9", "avtr_f9954c2b-3d99-4ef6-b1c0-d37a621daea4", "avtr_5edaf574-fe9a-479c-b667-fc810a9b6bd5", "avtr_a1d9bbc1-5043-4f8c-9a66-6c9f0a838ef0", "avtr_554b0cb3-d914-453a-ac2a-d708a805f341", "avtr_c93375d1-f263-426e-a9c9-e9c80f7d7845", "avtr_ab6ee78e-6f34-48f0-a2d0-e7f5800ca2f5", "avtr_bd423fd6-fc5b-43d3-8c7c-13f7d38c62a2", "avtr_c38a1615-5bf5-42b4-84eb-a8b6c37cbd11", "avtr_5a614921-04c2-464c-b686-65b69cc0d3af", "avtr_1ebec14b-c206-46c0-b109-784e028229c5", "avtr_3dcaa225-4fd8-42f0-9c37-97f063e733d1", "avtr_1d5a6c97-f3f3-48af-8ef5-f97cee6650da", "avtr_0910a30e-306d-4f0c-9f0d-81f1e958714d", "avtr_f6f1bd0e-f0b2-46c1-b726-0f6f21946c9f", "avtr_09d3e7d6-5ec5-4a63-be66-862f78a69f23", "avtr_b8d526fb-8810-4490-8dc9-d54af2febfe6", "avtr_2ba25aea-478c-41b2-9ea0-5719785bc990", "avtr_9b5043aa-602b-412e-a230-58b31946c895", "avtr_02bf6bfc-26a4-4dc0-92ea-07bc6a7ea02e", "avtr_20c931be-f685-4eb4-9041-c9bc75d4295b", "avtr_090faea3-4630-4cd7-b3a8-985b2975a91c", "avtr_dda7d2d0-1e02-4677-84d6-0e40d228ca6f", "avtr_1a4c9a8d-ee73-4b5a-ac75-092692716ad8", "avtr_a1410724-2875-422d-9323-293b0995b6b6", "avtr_cf8e0137-cc4b-4c7f-9a3d-a2935769ad57", "avtr_7d1005dd-b104-4c2c-9a43-d1db74318ef2", "avtr_5c9a1480-4f12-4546-92d1-42a917095fc9", "avtr_c64266a6-0095-42f0-ac01-0da0ca37f312", "avtr_2c6103e3-94c0-441c-8e54-1ed9e54621bd", "avtr_f3193cd0-e778-4263-ba78-0b7e2800ec6c", "avtr_2a2ad96b-fc31-4fc8-8fb8-bbc77db78576", "avtr_fee66f77-7ee2-49a0-a252-5c498663d64d", "avtr_fe5a176c-a877-4d64-8e97-aeedd30e3ea4", "avtr_290839f8-d8ec-489b-931a-28bd1d645ef8", "avtr_605b68ca-610d-4b24-b39b-cd05cfa9e361", "avtr_b4c8c559-5f87-4137-a342-e9b2a7c07978", "avtr_62811363-0d61-4f9e-9035-9ae6b2bd2c47", "avtr_4f47a678-20eb-42f0-b4f3-7be97072d106", "avtr_c87d0b23-20b4-48c7-8e9c-4b9f2d74d441", "avtr_bc8d18c5-5f11-40d2-83d8-dfbc0aee9d26", "avtr_877971f5-ad46-4d08-95c0-53371c8c2ff2", "avtr_2a530651-98d0-49d4-8e52-a1ad7858e0db", "avtr_0350e190-e272-4bd3-b8af-f749d6833f0d", "avtr_f2f8ceb4-fbd1-4c0b-abed-f01233f3cb9e", "avtr_9023220d-7a42-4fbf-b1b4-aadef94afc5e", "avtr_5f291837-1001-4a5a-a652-2a0296d82cd1", "avtr_3b5ecee2-cca3-4602-ba40-d6d1912cbd78", "avtr_3e05a046-de1a-46f2-8e17-8b079218bd76", "avtr_89e47835-b3d1-426e-8352-7f5f33f86ab6", "avtr_55c6d9c8-a4eb-4fff-b205-8edfec104f51", "avtr_2b4559b6-c6ba-41cc-b533-bed80818d72d", "avtr_1d624538-91f7-4ffb-b320-3fbafa3a05e7", "avtr_efbdcf2c-77a9-4d72-a6c0-e498a18b1b28", "avtr_a37310a1-b5d3-401b-ae79-b40d13ec597f", "avtr_158b96c5-f32d-4790-b44f-ef21c66c5be4", "avtr_c05b4b40-d072-4c4d-9fd4-bc413b408804", "avtr_59a8ab90-4810-4cc7-ba78-934667196a47", "avtr_398aaafe-14f0-45a1-8b0e-fdf821d5dbb3", "avtr_94ea3db6-1a20-465b-afd3-c8c2b938d12c", "avtr_e6c8678d-4807-406a-87a8-37e2e1b00d92", "avtr_3e8fbd8d-8de7-4e74-b798-d32d2e0f6fc6", "avtr_f281b959-d0cc-4cd1-968c-5f07e8be4a50", "avtr_de71d566-27f8-48cb-a526-4c6b4ecc3d29", "avtr_013f77de-7c23-463a-8f03-018fc1f343af", "avtr_4a7770ee-1524-4c7e-ba56-9f096f2b7f8e", "avtr_3f730379-807a-4883-aab3-52116fce94f3", "avtr_3fe6750e-eeef-4465-b522-77fc17823d57" ], - "instanceType": "Group Public", - "lastWorldBatteryLevel": -1, - "lastWorldBatteryLevelChange": 0, - "lastWorldBatteryStatus": "Charging", - "lastWorldGroupAccessType": "public", - "lastWorldId": "wrld_4b341546-65ff-4607-9d38-5b7f8f405132", - "lastWorldInstanceCode": "null", - "lastWorldInstanceDisplayName": "null", - "lastWorldInstanceGroupId": "grp_b6593dd3-6e86-4951-a8d8-e2fa3cb91096", - "lastWorldInstanceGroupName": "The FurVerse", - "lastWorldInstanceOccupants": 13, - "lastWorldInstanceType": "Group", - "lastWorldIsHome": false, - "lastWorldLegacyDeviceSeen": false, - "lastWorldLegacyInputUsed": false, - "lastWorldMeanTextMessageCharacters": 0, - "lastWorldMicToggleCount": 24, - "lastWorldName": "Furry Hideout", - "lastWorldNumberOfItemsReceivedDirectly": 0, - "lastWorldNumberOfItemsReceivedViaPedestal": 0, - "lastWorldNumberOfItemsSharedDirectly": 0, - "lastWorldNumberOfItemsSharedViaPedestal": 0, - "lastWorldNumberOfSharedItemsReported": 0, - "lastWorldNumberOfStickersCreated": 0, - "lastWorldNumberOfStickersPlaced": 0, - "lastWorldNumberOfUsersMet": 282, - "lastWorldNumGiftsGivenViaMassGift": 0, - "lastWorldNumMassGiftsLaunched": 0, - "lastWorldNumUsersGiftedViaMassGifting": 0, - "lastWorldPercentTimeSpentLoadingAvatars": 0.0870198458433151, - "lastWorldPrivateOccupants": 45, - "lastWorldProfilerSamples sample max ABDM Update": 11.3332996368408, - "lastWorldProfilerSamples sample max AC Reset": 50.2845993041992, - "lastWorldProfilerSamples sample max AC Update": 0.347600013017654, - "lastWorldProfilerSamples sample max ACl Anim": 303.517608642578, - "lastWorldProfilerSamples sample max ACl Anim Copy Layer Weights": 0.284299999475479, - "lastWorldProfilerSamples sample max ACl Anim Copy Params": 3.17810010910034, - "lastWorldProfilerSamples sample max ACl Ext Blend Shapes": 0.344300001859665, - "lastWorldProfilerSamples sample max ACl Scale Sen": 0.241600006818771, - "lastWorldProfilerSamples sample max ACl Target Pos": 3.01889991760254, - "lastWorldProfilerSamples sample max AMg Instantiate": 17.8307991027832, - "lastWorldProfilerSamples sample max AMg Material": 0.0318000018596649, - "lastWorldProfilerSamples sample max AMg Shader": 0.00240000011399388, - "lastWorldProfilerSamples sample max Av Audio": 0.85970002412796, - "lastWorldProfilerSamples sample max Av Chop": 0.483700007200241, - "lastWorldProfilerSamples sample max Av Cull": 2.2658998966217, - "lastWorldProfilerSamples sample max Av Head Scale": 0.301899999380112, - "lastWorldProfilerSamples sample max Av Limit Particle Systems": 1.1923999786377, - "lastWorldProfilerSamples sample max Av Safety": 0.761300027370453, - "lastWorldProfilerSamples sample max Av Shaders": 5.54860019683838, - "lastWorldProfilerSamples sample max Av Stop Particle Systems": 5.5310001373291, - "lastWorldProfilerSamples sample max Av Visibility": 4.67210006713867, - "lastWorldProfilerSamples sample max AV2 IK": 0.286100000143051, - "lastWorldProfilerSamples sample max AV2 NoIK": 0.000699999975040555, - "lastWorldProfilerSamples sample max AV3 Extract": 1.32379996776581, - "lastWorldProfilerSamples sample max AV3 IK": 3.18740010261536, - "lastWorldProfilerSamples sample max AV3 NoIK": 0.0549000017344952, - "lastWorldProfilerSamples sample max Campaign Manager Update": 0.799199998378754, - "lastWorldProfilerSamples sample max CNP Update": 0.951799988746643, - "lastWorldProfilerSamples sample max CusEmo Callback": 1.09889996051788, - "lastWorldProfilerSamples sample max CusEmo Mask": 0.427599996328354, - "lastWorldProfilerSamples sample max CusSti Callback": 0.0185000002384186, - "lastWorldProfilerSamples sample max CusSti Mask": 2.10229992866516, - "lastWorldProfilerSamples sample max E Tick": 3.58550000190735, - "lastWorldProfilerSamples sample max EOS Update": 0.212200000882149, - "lastWorldProfilerSamples sample max EP Update": 2.03379988670349, - "lastWorldProfilerSamples sample max EvL Clear": 0.164800003170967, - "lastWorldProfilerSamples sample max EvL E SendRPC": 3.63980007171631, - "lastWorldProfilerSamples sample max EvL New Event": 0.617799997329712, - "lastWorldProfilerSamples sample max EvL Record and Broadcast": 0.284099996089935, - "lastWorldProfilerSamples sample max EvL Update": 3.6784999370575, - "lastWorldProfilerSamples sample max EvRp Event": 0.231299996376038, - "lastWorldProfilerSamples sample max EvRp Process": 0.230599999427795, - "lastWorldProfilerSamples sample max EyeAnim Cache": 0.0015999999595806, - "lastWorldProfilerSamples sample max FBN Decode": 32.0046005249023, - "lastWorldProfilerSamples sample max FBN Player Order": 5.10449981689453, - "lastWorldProfilerSamples sample max FBN Receive": 36.0761985778809, - "lastWorldProfilerSamples sample max FBN Update": 3.51990008354187, - "lastWorldProfilerSamples sample max Flow Core Ready": 0.00730000017210841, - "lastWorldProfilerSamples sample max Flow Is Core": 0.0203000009059906, - "lastWorldProfilerSamples sample max GC AllocateMegabytes": 61.7835998535156, - "lastWorldProfilerSamples sample max GC Collect": 99.2211990356445, - "lastWorldProfilerSamples sample max GC EnsureHeapSize": 162.422103881836, - "lastWorldProfilerSamples sample max HpCm Logic": 0.231800004839897, - "lastWorldProfilerSamples sample max HUD": 0.209900006651878, - "lastWorldProfilerSamples sample max IKC Update": 3.56539988517761, - "lastWorldProfilerSamples sample max MoS Update": 0.269600003957748, - "lastWorldProfilerSamples sample max Net Thread Mgr": 0.206400007009506, - "lastWorldProfilerSamples sample max Networking Book": 2.46569991111755, - "lastWorldProfilerSamples sample max Networking Cache Copy and Elimination": 55.9859008789063, - "lastWorldProfilerSamples sample max Networking Cache Processing": 35.3070983886719, - "lastWorldProfilerSamples sample max Networking Flush": 62.3492012023926, - "lastWorldProfilerSamples sample max Networking Incoming": 142.934799194336, - "lastWorldProfilerSamples sample max Networking Suffer": 3.12260007858276, - "lastWorldProfilerSamples sample max NP Calc": 0.479699999094009, - "lastWorldProfilerSamples sample max NP Material": 1.69939994812012, - "lastWorldProfilerSamples sample max NP Offset": 22.5720996856689, - "lastWorldProfilerSamples sample max NP Voice": 0.342200011014938, - "lastWorldProfilerSamples sample max OG Update": 40.1743011474609, - "lastWorldProfilerSamples sample max PL Update": 4.10349988937378, - "lastWorldProfilerSamples sample max PNP Update": 0.486099988222122, - "lastWorldProfilerSamples sample max Pose AV2 Apply": 0.451999992132187, - "lastWorldProfilerSamples sample max Pose AV3 Apply": 4.0770001411438, - "lastWorldProfilerSamples sample max Pose Local Update Position": 0.0383000001311302, - "lastWorldProfilerSamples sample max Pose Update": 40.2501983642578, - "lastWorldProfilerSamples sample max PRc Apply": 4.07600021362305, - "lastWorldProfilerSamples sample max PRc ApplyIK": 3.19510006904602, - "lastWorldProfilerSamples sample max PRc Motion": 1.34619998931885, - "lastWorldProfilerSamples sample max PRc Position": 1.92980003356934, - "lastWorldProfilerSamples sample max PRc PostApply": 3.13860011100769, - "lastWorldProfilerSamples sample max PRc Puppet": 0.197500005364418, - "lastWorldProfilerSamples sample max PRc ShouldApplyIK": 4.05940008163452, - "lastWorldProfilerSamples sample max PRc Tween": 40.2490005493164, - "lastWorldProfilerSamples sample max PRc Tween IK": 39.4314994812012, - "lastWorldProfilerSamples sample max PRc Tween Physics": 40.230899810791, - "lastWorldProfilerSamples sample max Prt Update": 37.2000007629395, - "lastWorldProfilerSamples sample max PStS Update": 2.36470007896423, - "lastWorldProfilerSamples sample max Settings Update": 0.284700006246567, - "lastWorldProfilerSamples sample max SimT Update": 2.33610010147095, - "lastWorldProfilerSamples sample max SVR2 Apply": 2.94519996643066, - "lastWorldProfilerSamples sample max SyncPhys Update": 23.425500869751, - "lastWorldProfilerSamples sample max Towards Player Update": 0.493499994277954, - "lastWorldProfilerSamples sample max USp 3DPan": 0.190599992871284, - "lastWorldProfilerSamples sample max USp AutoLevel": 0.306600004434586, - "lastWorldProfilerSamples sample max USp bandMode": 0.146300002932549, - "lastWorldProfilerSamples sample max USp bitRate": 0.144299998879433, - "lastWorldProfilerSamples sample max USp Check": 0.207000002264977, - "lastWorldProfilerSamples sample max USp ConeAttenuation": 3.49920010566711, - "lastWorldProfilerSamples sample max USp Deserialize": 5.54930019378662, - "lastWorldProfilerSamples sample max USp Get Data": 0.293300002813339, - "lastWorldProfilerSamples sample max Usp Get Silence": 0.510900020599365, - "lastWorldProfilerSamples sample max USp isPlaying": 1.79849994182587, - "lastWorldProfilerSamples sample max USp last3DMode": 0.20610000193119, - "lastWorldProfilerSamples sample max USp Process": 27.512300491333, - "lastWorldProfilerSamples sample max USp Queue": 2.50419998168945, - "lastWorldProfilerSamples sample max USp Receive": 27.5475997924805, - "lastWorldProfilerSamples sample max USp recording": 111.370697021484, - "lastWorldProfilerSamples sample max USp send": 0.207300007343292, - "lastWorldProfilerSamples sample max USp SortVoices": 0.804799973964691, - "lastWorldProfilerSamples sample max USp TotalVoiceHeardTime": 0.22409999370575, - "lastWorldProfilerSamples sample max USp TrackedBandwidthUsage": 0.169300004839897, - "lastWorldProfilerSamples sample max USp visemeAudio": 0.695200026035309, - "lastWorldProfilerSamples sample max USp VoicePriority": 0.361400008201599, - "lastWorldProfilerSamples sample max USync Update": 0.611599981784821, - "lastWorldProfilerSamples sample max Validation Audio Source Limits": 1.58580005168915, - "lastWorldProfilerSamples sample max Validation Avatar Dynamics Limits": 0.895600020885468, - "lastWorldProfilerSamples sample max Validation Avatar IK Limits": 3.11350011825562, - "lastWorldProfilerSamples sample max Validation Avatar Station Limits": 1.07140004634857, - "lastWorldProfilerSamples sample mean ABDM Update": 0.00825389195233583, - "lastWorldProfilerSamples sample mean AC Reset": 0.12819804251194, - "lastWorldProfilerSamples sample mean AC Update": 0.000255414139246568, - "lastWorldProfilerSamples sample mean ACl Anim": 0.134653970599174, - "lastWorldProfilerSamples sample mean ACl Anim Copy Layer Weights": 0.00200919038616121, - "lastWorldProfilerSamples sample mean ACl Anim Copy Params": 0.131441861391068, - "lastWorldProfilerSamples sample mean ACl Ext Blend Shapes": 0.0123360455036163, - "lastWorldProfilerSamples sample mean ACl Scale Sen": 0.000278322957456112, - "lastWorldProfilerSamples sample mean ACl Target Pos": 0.0186093300580978, - "lastWorldProfilerSamples sample mean AMg Instantiate": 0.219471171498299, - "lastWorldProfilerSamples sample mean AMg Material": 0.000265810813289136, - "lastWorldProfilerSamples sample mean AMg Shader": 0.00446833809837699, - "lastWorldProfilerSamples sample mean Av Audio": 0.00106428610160947, - "lastWorldProfilerSamples sample mean Av Chop": 0.000322727311868221, - "lastWorldProfilerSamples sample mean Av Cull": 0.00373778282664716, - "lastWorldProfilerSamples sample mean Av Head Scale": 0.0016113935271278, - "lastWorldProfilerSamples sample mean Av Limit Particle Systems": 0.000447339087259024, - "lastWorldProfilerSamples sample mean Av Safety": 0.000217898967093788, - "lastWorldProfilerSamples sample mean Av Shaders": 0.0116419950500131, - "lastWorldProfilerSamples sample mean Av Stop Particle Systems": 0.00155367306433618, - "lastWorldProfilerSamples sample mean Av Visibility": 0.00136836222372949, - "lastWorldProfilerSamples sample mean AV2 IK": 0.0369756035506725, - "lastWorldProfilerSamples sample mean AV2 NoIK": 0.000201617323909886, - "lastWorldProfilerSamples sample mean AV3 Extract": 0.00144772790372372, - "lastWorldProfilerSamples sample mean AV3 IK": 0.0021360507234931, - "lastWorldProfilerSamples sample mean AV3 NoIK": 0.000788805889897048, - "lastWorldProfilerSamples sample mean Campaign Manager Update": 0.00115072599146515, - "lastWorldProfilerSamples sample mean CNP Update": 0.00088111066725105, - "lastWorldProfilerSamples sample mean CusEmo Callback": 0.123893335461617, - "lastWorldProfilerSamples sample mean CusEmo Mask": 0.154306679964066, - "lastWorldProfilerSamples sample mean CusSti Callback": 0.0106909088790417, - "lastWorldProfilerSamples sample mean CusSti Mask": 0.378868192434311, - "lastWorldProfilerSamples sample mean E Tick": 0.0474084839224815, - "lastWorldProfilerSamples sample mean EOS Update": 0.000704330974258482, - "lastWorldProfilerSamples sample mean EP Update": 0.000202885872568004, - "lastWorldProfilerSamples sample mean EvL Clear": 0.000824328628368676, - "lastWorldProfilerSamples sample mean EvL E SendRPC": 0.431086540222168, - "lastWorldProfilerSamples sample mean EvL New Event": 0.0113730365410447, - "lastWorldProfilerSamples sample mean EvL Record and Broadcast": 0.0667460933327675, - "lastWorldProfilerSamples sample mean EvL Update": 0.00123328098561615, - "lastWorldProfilerSamples sample mean EvRp Event": 7.64396681915969E-05, - "lastWorldProfilerSamples sample mean EvRp Process": 0.0666719526052475, - "lastWorldProfilerSamples sample mean EyeAnim Cache": 0.00172842503525317, - "lastWorldProfilerSamples sample mean FBN Decode": 0.0243430156260729, - "lastWorldProfilerSamples sample mean FBN Player Order": 0.0429155603051186, - "lastWorldProfilerSamples sample mean FBN Receive": 0.0321900993585587, - "lastWorldProfilerSamples sample mean FBN Update": 0.00324487779289484, - "lastWorldProfilerSamples sample mean Flow Core Ready": 0.0035749850794673, - "lastWorldProfilerSamples sample mean Flow Is Core": 0.000782304676249623, - "lastWorldProfilerSamples sample mean GC AllocateMegabytes": 61.7835998535156, - "lastWorldProfilerSamples sample mean GC Collect": 0.00034518085885793, - "lastWorldProfilerSamples sample mean GC EnsureHeapSize": 162.422103881836, - "lastWorldProfilerSamples sample mean HpCm Logic": 0.000719358213245869, - "lastWorldProfilerSamples sample mean HUD": 0.000925653730519116, - "lastWorldProfilerSamples sample mean IKC Update": 0.000549282704014331, - "lastWorldProfilerSamples sample mean MoS Update": 0.000208585712243803, - "lastWorldProfilerSamples sample mean Net Thread Mgr": 0.000676508236210793, - "lastWorldProfilerSamples sample mean Networking Book": 0.179487019777298, - "lastWorldProfilerSamples sample mean Networking Cache Copy and Elimination": 0.00238762819208205, - "lastWorldProfilerSamples sample mean Networking Cache Processing": 0.0188484024256468, - "lastWorldProfilerSamples sample mean Networking Flush": 0.0271810535341501, - "lastWorldProfilerSamples sample mean Networking Incoming": 0.207722052931786, - "lastWorldProfilerSamples sample mean Networking Suffer": 0.00221073441207409, - "lastWorldProfilerSamples sample mean NP Calc": 0.00088070600759238, - "lastWorldProfilerSamples sample mean NP Material": 0.00270290346816182, - "lastWorldProfilerSamples sample mean NP Offset": 0.0648431032896042, - "lastWorldProfilerSamples sample mean NP Voice": 0.000737966562155634, - "lastWorldProfilerSamples sample mean OG Update": 0.0908304750919342, - "lastWorldProfilerSamples sample mean PL Update": 0.000283868022961542, - "lastWorldProfilerSamples sample mean PNP Update": 0.00159345543943346, - "lastWorldProfilerSamples sample mean Pose AV2 Apply": 0.0322666428983212, - "lastWorldProfilerSamples sample mean Pose AV3 Apply": 0.00465629529207945, - "lastWorldProfilerSamples sample mean Pose Local Update Position": 0.00339872040785849, - "lastWorldProfilerSamples sample mean Pose Update": 0.00857232417911291, - "lastWorldProfilerSamples sample mean PRc Apply": 0.00686609419062734, - "lastWorldProfilerSamples sample mean PRc ApplyIK": 0.0048172096721828, - "lastWorldProfilerSamples sample mean PRc Motion": 0.00215315166860819, - "lastWorldProfilerSamples sample mean PRc Position": 0.0020877686329186, - "lastWorldProfilerSamples sample mean PRc PostApply": 0.00294443918392062, - "lastWorldProfilerSamples sample mean PRc Puppet": 0.000134984351461753, - "lastWorldProfilerSamples sample mean PRc ShouldApplyIK": 0.000802272581495345, - "lastWorldProfilerSamples sample mean PRc Tween": 0.00771516328677535, - "lastWorldProfilerSamples sample mean PRc Tween IK": 0.00442713173106313, - "lastWorldProfilerSamples sample mean PRc Tween Physics": 0.00273716868832707, - "lastWorldProfilerSamples sample mean Prt Update": 0.0029079164378345, - "lastWorldProfilerSamples sample mean PStS Update": 0.000273040204774588, - "lastWorldProfilerSamples sample mean Settings Update": 0.00250091939233243, - "lastWorldProfilerSamples sample mean SimT Update": 0.000537645246367902, - "lastWorldProfilerSamples sample mean SVR2 Apply": 0.0348070561885834, - "lastWorldProfilerSamples sample mean SyncPhys Update": 0.00103959569241852, - "lastWorldProfilerSamples sample mean Towards Player Update": 0.00161896692588925, - "lastWorldProfilerSamples sample mean USp 3DPan": 2.81340689980425E-05, - "lastWorldProfilerSamples sample mean USp AutoLevel": 0.00128505064640194, - "lastWorldProfilerSamples sample mean USp bandMode": 0.000209776771953329, - "lastWorldProfilerSamples sample mean USp bitRate": 0.000379965815227479, - "lastWorldProfilerSamples sample mean USp Check": 0.000553109333850443, - "lastWorldProfilerSamples sample mean USp ConeAttenuation": 0.00078820763155818, - "lastWorldProfilerSamples sample mean USp Deserialize": 0.0122510707005858, - "lastWorldProfilerSamples sample mean USp Get Data": 0.00227852980606258, - "lastWorldProfilerSamples sample mean Usp Get Silence": 9.60440665949136E-05, - "lastWorldProfilerSamples sample mean USp isPlaying": 0.000208407276659273, - "lastWorldProfilerSamples sample mean USp last3DMode": 9.23745392356068E-05, - "lastWorldProfilerSamples sample mean USp Process": 0.13715834915638, - "lastWorldProfilerSamples sample mean USp Queue": 0.00485154567286372, - "lastWorldProfilerSamples sample mean USp Receive": 0.138330861926079, - "lastWorldProfilerSamples sample mean USp recording": 0.0190036557614803, - "lastWorldProfilerSamples sample mean USp send": 0.00114579906221479, - "lastWorldProfilerSamples sample mean USp SortVoices": 0.011019004508853, - "lastWorldProfilerSamples sample mean USp TotalVoiceHeardTime": 0.000973763060756028, - "lastWorldProfilerSamples sample mean USp TrackedBandwidthUsage": 0.000512772705405951, - "lastWorldProfilerSamples sample mean USp visemeAudio": 0.00845629442483187, - "lastWorldProfilerSamples sample mean USp VoicePriority": 0.000319766550092027, - "lastWorldProfilerSamples sample mean USync Update": 0.000408728199545294, - "lastWorldProfilerSamples sample mean Validation Audio Source Limits": 0.151965960860252, - "lastWorldProfilerSamples sample mean Validation Avatar Dynamics Limits": 0.058777891099453, - "lastWorldProfilerSamples sample mean Validation Avatar IK Limits": 0.118672981858253, - "lastWorldProfilerSamples sample mean Validation Avatar Station Limits": 0.0143002020195127, - "lastWorldProfilerSamples sample timeWeightedMean ABDM Update": 0.022380243986845, - "lastWorldProfilerSamples sample timeWeightedMean AC Reset": 5.27340841293335, - "lastWorldProfilerSamples sample timeWeightedMean AC Update": 0.000223345545236953, - "lastWorldProfilerSamples sample timeWeightedMean ACl Anim": 1.21592772006989, - "lastWorldProfilerSamples sample timeWeightedMean ACl Anim Copy Layer Weights": 0.0020342965144664, - "lastWorldProfilerSamples sample timeWeightedMean ACl Anim Copy Params": 0.135797262191772, - "lastWorldProfilerSamples sample timeWeightedMean ACl Ext Blend Shapes": 0.0114491116255522, - "lastWorldProfilerSamples sample timeWeightedMean ACl Scale Sen": 0.000152473556227051, - "lastWorldProfilerSamples sample timeWeightedMean ACl Target Pos": 0.0194577611982822, - "lastWorldProfilerSamples sample timeWeightedMean AMg Instantiate": 0.70631605386734, - "lastWorldProfilerSamples sample timeWeightedMean AMg Material": 0.000182833886356093, - "lastWorldProfilerSamples sample timeWeightedMean AMg Shader": 0.000842937501147389, - "lastWorldProfilerSamples sample timeWeightedMean Av Audio": 0.00119451934006065, - "lastWorldProfilerSamples sample timeWeightedMean Av Chop": 0.000353201874531806, - "lastWorldProfilerSamples sample timeWeightedMean Av Cull": 0.00458679767325521, - "lastWorldProfilerSamples sample timeWeightedMean Av Head Scale": 0.00142840994521976, - "lastWorldProfilerSamples sample timeWeightedMean Av Limit Particle Systems": 0.000419857242377475, - "lastWorldProfilerSamples sample timeWeightedMean Av Safety": 0.000289838237222284, - "lastWorldProfilerSamples sample timeWeightedMean Av Shaders": 0.0174866449087858, - "lastWorldProfilerSamples sample timeWeightedMean Av Stop Particle Systems": 0.00177104468457401, - "lastWorldProfilerSamples sample timeWeightedMean Av Visibility": 0.00219838647171855, - "lastWorldProfilerSamples sample timeWeightedMean AV2 IK": 0.0371598638594151, - "lastWorldProfilerSamples sample timeWeightedMean AV2 NoIK": 0.000208627985557541, - "lastWorldProfilerSamples sample timeWeightedMean AV3 Extract": 0.00172583793755621, - "lastWorldProfilerSamples sample timeWeightedMean AV3 IK": 0.00235766428522766, - "lastWorldProfilerSamples sample timeWeightedMean AV3 NoIK": 0.000951852707657963, - "lastWorldProfilerSamples sample timeWeightedMean Campaign Manager Update": 0.000696219853125513, - "lastWorldProfilerSamples sample timeWeightedMean CNP Update": 0.00101236382033676, - "lastWorldProfilerSamples sample timeWeightedMean CusEmo Callback": 0.200682654976845, - "lastWorldProfilerSamples sample timeWeightedMean CusEmo Mask": 0.168094098567963, - "lastWorldProfilerSamples sample timeWeightedMean CusSti Callback": 0.0106572173535824, - "lastWorldProfilerSamples sample timeWeightedMean CusSti Mask": 0.361865401268005, - "lastWorldProfilerSamples sample timeWeightedMean E Tick": 0.0477200113236904, - "lastWorldProfilerSamples sample timeWeightedMean EOS Update": 0.000623073545284569, - "lastWorldProfilerSamples sample timeWeightedMean EP Update": 0.000222490911255591, - "lastWorldProfilerSamples sample timeWeightedMean EvL Clear": 0.00416948739439249, - "lastWorldProfilerSamples sample timeWeightedMean EvL E SendRPC": 0.551252961158752, - "lastWorldProfilerSamples sample timeWeightedMean EvL New Event": 0.0242984611541033, - "lastWorldProfilerSamples sample timeWeightedMean EvL Record and Broadcast": 0.0952660441398621, - "lastWorldProfilerSamples sample timeWeightedMean EvL Update": 0.0049300966784358, - "lastWorldProfilerSamples sample timeWeightedMean EvRp Event": 8.66483169374987E-05, - "lastWorldProfilerSamples sample timeWeightedMean EvRp Process": 0.0681881755590439, - "lastWorldProfilerSamples sample timeWeightedMean EyeAnim Cache": 0.000209323887247592, - "lastWorldProfilerSamples sample timeWeightedMean FBN Decode": 0.0249152798205614, - "lastWorldProfilerSamples sample timeWeightedMean FBN Player Order": 0.0490005984902382, - "lastWorldProfilerSamples sample timeWeightedMean FBN Receive": 0.0353693589568138, - "lastWorldProfilerSamples sample timeWeightedMean FBN Update": 0.00713661266490817, - "lastWorldProfilerSamples sample timeWeightedMean Flow Core Ready": 0.00730000017210841, - "lastWorldProfilerSamples sample timeWeightedMean Flow Is Core": 0.000258336251135916, - "lastWorldProfilerSamples sample timeWeightedMean GC AllocateMegabytes": 61.7835998535156, - "lastWorldProfilerSamples sample timeWeightedMean GC Collect": 88.5117568969727, - "lastWorldProfilerSamples sample timeWeightedMean GC EnsureHeapSize": 162.422103881836, - "lastWorldProfilerSamples sample timeWeightedMean HpCm Logic": 0.000656014541164041, - "lastWorldProfilerSamples sample timeWeightedMean HUD": 0.00114993483293802, - "lastWorldProfilerSamples sample timeWeightedMean IKC Update": 0.000640570709947497, - "lastWorldProfilerSamples sample timeWeightedMean MoS Update": 0.000194310414372012, - "lastWorldProfilerSamples sample timeWeightedMean Net Thread Mgr": 0.000671765301376581, - "lastWorldProfilerSamples sample timeWeightedMean Networking Book": 0.465297669172287, - "lastWorldProfilerSamples sample timeWeightedMean Networking Cache Copy and Elimination": 0.0123689407482743, - "lastWorldProfilerSamples sample timeWeightedMean Networking Cache Processing": 0.0207530315965414, - "lastWorldProfilerSamples sample timeWeightedMean Networking Flush": 0.0456199236214161, - "lastWorldProfilerSamples sample timeWeightedMean Networking Incoming": 0.264092117547989, - "lastWorldProfilerSamples sample timeWeightedMean Networking Suffer": 0.00238781282678246, - "lastWorldProfilerSamples sample timeWeightedMean NP Calc": 0.00103627529460937, - "lastWorldProfilerSamples sample timeWeightedMean NP Material": 0.00238068494945765, - "lastWorldProfilerSamples sample timeWeightedMean NP Offset": 2.56693601608276, - "lastWorldProfilerSamples sample timeWeightedMean NP Voice": 0.000824272923637182, - "lastWorldProfilerSamples sample timeWeightedMean OG Update": 0.121435955166817, - "lastWorldProfilerSamples sample timeWeightedMean PL Update": 0.000383810431230813, - "lastWorldProfilerSamples sample timeWeightedMean PNP Update": 0.0018012000946328, - "lastWorldProfilerSamples sample timeWeightedMean Pose AV2 Apply": 0.0374916233122349, - "lastWorldProfilerSamples sample timeWeightedMean Pose AV3 Apply": 0.00449713971465826, - "lastWorldProfilerSamples sample timeWeightedMean Pose Local Update Position": 0.0328157246112823, - "lastWorldProfilerSamples sample timeWeightedMean Pose Update": 0.0454562269151211, - "lastWorldProfilerSamples sample timeWeightedMean PRc Apply": 0.0111646978184581, - "lastWorldProfilerSamples sample timeWeightedMean PRc ApplyIK": 0.00653970800340176, - "lastWorldProfilerSamples sample timeWeightedMean PRc Motion": 0.00266523729078472, - "lastWorldProfilerSamples sample timeWeightedMean PRc Position": 0.00306479539722204, - "lastWorldProfilerSamples sample timeWeightedMean PRc PostApply": 0.00307173328474164, - "lastWorldProfilerSamples sample timeWeightedMean PRc Puppet": 5.8727600844577E-05, - "lastWorldProfilerSamples sample timeWeightedMean PRc ShouldApplyIK": 0.00166922935750335, - "lastWorldProfilerSamples sample timeWeightedMean PRc Tween": 0.0455961972475052, - "lastWorldProfilerSamples sample timeWeightedMean PRc Tween IK": 0.0385871790349483, - "lastWorldProfilerSamples sample timeWeightedMean PRc Tween Physics": 0.0121766198426485, - "lastWorldProfilerSamples sample timeWeightedMean Prt Update": 0.0134113654494286, - "lastWorldProfilerSamples sample timeWeightedMean PStS Update": 0.000282001245068386, - "lastWorldProfilerSamples sample timeWeightedMean Settings Update": 0.0022705988958478, - "lastWorldProfilerSamples sample timeWeightedMean SimT Update": 0.000727078295312822, - "lastWorldProfilerSamples sample timeWeightedMean SVR2 Apply": 0.0354856662452221, - "lastWorldProfilerSamples sample timeWeightedMean SyncPhys Update": 0.00155976915266365, - "lastWorldProfilerSamples sample timeWeightedMean Towards Player Update": 0.00163868244271725, - "lastWorldProfilerSamples sample timeWeightedMean USp 3DPan": 5.84455083298963E-05, - "lastWorldProfilerSamples sample timeWeightedMean USp AutoLevel": 0.0012811494525522, - "lastWorldProfilerSamples sample timeWeightedMean USp bandMode": 0.000127738443552516, - "lastWorldProfilerSamples sample timeWeightedMean USp bitRate": 0.000106033126940019, - "lastWorldProfilerSamples sample timeWeightedMean USp Check": 0.000554782163817436, - "lastWorldProfilerSamples sample timeWeightedMean USp ConeAttenuation": 0.000957603275310248, - "lastWorldProfilerSamples sample timeWeightedMean USp Deserialize": 0.0125984102487564, - "lastWorldProfilerSamples sample timeWeightedMean USp Get Data": 0.00228934804908931, - "lastWorldProfilerSamples sample timeWeightedMean Usp Get Silence": 0.000105945480754599, - "lastWorldProfilerSamples sample timeWeightedMean USp isPlaying": 0.000252956320764497, - "lastWorldProfilerSamples sample timeWeightedMean USp last3DMode": 9.34544077608734E-05, - "lastWorldProfilerSamples sample timeWeightedMean USp Process": 0.140874981880188, - "lastWorldProfilerSamples sample timeWeightedMean USp Queue": 0.00549836736172438, - "lastWorldProfilerSamples sample timeWeightedMean USp Receive": 0.142099291086197, - "lastWorldProfilerSamples sample timeWeightedMean USp recording": 0.049074649810791, - "lastWorldProfilerSamples sample timeWeightedMean USp send": 0.00128652527928352, - "lastWorldProfilerSamples sample timeWeightedMean USp SortVoices": 0.0110221542418003, - "lastWorldProfilerSamples sample timeWeightedMean USp TotalVoiceHeardTime": 0.000996055547147989, - "lastWorldProfilerSamples sample timeWeightedMean USp TrackedBandwidthUsage": 0.000149930099723861, - "lastWorldProfilerSamples sample timeWeightedMean USp visemeAudio": 0.00968470610678196, - "lastWorldProfilerSamples sample timeWeightedMean USp VoicePriority": 0.000342641054885462, - "lastWorldProfilerSamples sample timeWeightedMean USync Update": 0.000546450261026621, - "lastWorldProfilerSamples sample timeWeightedMean Validation Audio Source Limits": 0.194457799196243, - "lastWorldProfilerSamples sample timeWeightedMean Validation Avatar Dynamics Limits": 0.0810618102550507, - "lastWorldProfilerSamples sample timeWeightedMean Validation Avatar IK Limits": 0.161890536546707, - "lastWorldProfilerSamples sample timeWeightedMean Validation Avatar Station Limits": 0.0147019969299436, - "lastWorldPublicOccupants": 615, - "lastWorldRegion": "use", - "lastWorldSDKVersion": "SDK3", - "lastWorldStats runningTime": 9905.9140625, - "lastWorldStats stat max avatar_kind_Blocked": 0, - "lastWorldStats stat max avatar_kind_Custom": 51, - "lastWorldStats stat max avatar_kind_Error": 0, - "lastWorldStats stat max avatar_kind_Fallback": 0, - "lastWorldStats stat max avatar_kind_Filtered": 0, - "lastWorldStats stat max avatar_kind_Impostor": 0, - "lastWorldStats stat max avatar_kind_Loading": 7, - "lastWorldStats stat max avatar_kind_Performance": 31, - "lastWorldStats stat max avatar_kind_Safety": 0, - "lastWorldStats stat max avatar_kind_Substitute": 0, - "lastWorldStats stat max avatar_kind_Undefined": 0, - "lastWorldStats stat max avatar_perf_rating_Excellent": 7, - "lastWorldStats stat max avatar_perf_rating_Good": 9, - "lastWorldStats stat max avatar_perf_rating_Medium": 9, - "lastWorldStats stat max avatar_perf_rating_None": 35, - "lastWorldStats stat max avatar_perf_rating_Poor": 5, - "lastWorldStats stat max avatar_perf_rating_VeryPoor": 39, - "lastWorldStats stat max avatar_proxy": 35, - "lastWorldStats stat max cpu_frame_time": 3.1321234703064, - "lastWorldStats stat max fps": 85.1854019165039, - "lastWorldStats stat max gpu_frame_time": 0.06292724609375, - "lastWorldStats stat max input_latency": 3135.09497070313, - "lastWorldStats stat max memory_managed_used": 1010077696, - "lastWorldStats stat max mutual_player_nameplates_visible": 0, - "lastWorldStats stat max ping": 80, - "lastWorldStats stat max player_count": 57, - "lastWorldStats stat max texture_memory_current": 5452110848, - "lastWorldStats stat max texture_memory_desired": 4149029888, - "lastWorldStats stat max texture_memory_nonstreaming": 2832417792, - "lastWorldStats stat max texture_memory_saved_by_streaming": 3736293888, - "lastWorldStats stat max texture_memory_total": 8855646208, - "lastWorldStats stat max visible_avatar_animator_count": 49, - "lastWorldStats stat max visible_avatar_audio_source_count": 297, - "lastWorldStats stat max visible_avatar_bone_count": 8291, - "lastWorldStats stat max visible_avatar_cloth_count": 1, - "lastWorldStats stat max visible_avatar_cloth_vertices_count": 502, - "lastWorldStats stat max visible_avatar_constraint_count": 118, - "lastWorldStats stat max visible_avatar_constraint_depth": 433, - "lastWorldStats stat max visible_avatar_contact_count": 560, - "lastWorldStats stat max visible_avatar_light_count": 134, - "lastWorldStats stat max visible_avatar_line_renderer_count": 6, - "lastWorldStats stat max visible_avatar_material_count": 1323, - "lastWorldStats stat max visible_avatar_mesh_count": 325, - "lastWorldStats stat max visible_avatar_particle_mesh_poly_count": 7296796, - "lastWorldStats stat max visible_avatar_particle_system_count": 496, - "lastWorldStats stat max visible_avatar_particle_total_count": 151738, - "lastWorldStats stat max visible_avatar_physbone_collider_count": 181, - "lastWorldStats stat max visible_avatar_physbone_collision_count": 6962, - "lastWorldStats stat max visible_avatar_physbone_component_count": 829, - "lastWorldStats stat max visible_avatar_physbone_transform_count": 5217, - "lastWorldStats stat max visible_avatar_physics_collider_count": 21, - "lastWorldStats stat max visible_avatar_physics_rigidbody_count": 13, - "lastWorldStats stat max visible_avatar_poly_count": 6438321, - "lastWorldStats stat max visible_avatar_skinned_mesh_count": 416, - "lastWorldStats stat max visible_avatar_trail_renderer_count": 18, - "lastWorldStats stat max visible_player_count": 22, - "lastWorldStats stat mean avatar_kind_Blocked": 0, - "lastWorldStats stat mean avatar_kind_Custom": 30.6645374298096, - "lastWorldStats stat mean avatar_kind_Error": 0, - "lastWorldStats stat mean avatar_kind_Fallback": 0, - "lastWorldStats stat mean avatar_kind_Filtered": 0, - "lastWorldStats stat mean avatar_kind_Impostor": 0, - "lastWorldStats stat mean avatar_kind_Loading": 0.0958680883049965, - "lastWorldStats stat mean avatar_kind_Performance": 2.38661217689514, - "lastWorldStats stat mean avatar_kind_Safety": 0, - "lastWorldStats stat mean avatar_kind_Substitute": 0, - "lastWorldStats stat mean avatar_kind_Undefined": 0, - "lastWorldStats stat mean avatar_perf_rating_Excellent": 1.458939909935, - "lastWorldStats stat mean avatar_perf_rating_Good": 4.21768140792847, - "lastWorldStats stat mean avatar_perf_rating_Medium": 5.52951908111572, - "lastWorldStats stat mean avatar_perf_rating_None": 1.05522465705872, - "lastWorldStats stat mean avatar_perf_rating_Poor": 1.24957120418549, - "lastWorldStats stat mean avatar_perf_rating_VeryPoor": 19.6217727661133, - "lastWorldStats stat mean avatar_proxy": 12.7212018966675, - "lastWorldStats stat mean cpu_frame_time": 0.0210597403347492, - "lastWorldStats stat mean fps": 46.8701667785645, - "lastWorldStats stat mean gpu_frame_time": 0.00111912551801652, - "lastWorldStats stat mean input_latency": 21.5352478027344, - "lastWorldStats stat mean memory_managed_used": 798112256, - "lastWorldStats stat mean mutual_player_nameplates_visible": 0, - "lastWorldStats stat mean ping": 56.045654296875, - "lastWorldStats stat mean player_count": 33.4626197814941, - "lastWorldStats stat mean texture_memory_current": 3207652608, - "lastWorldStats stat mean texture_memory_desired": 2639664128, - "lastWorldStats stat mean texture_memory_nonstreaming": 1893669760, - "lastWorldStats stat mean texture_memory_saved_by_streaming": 1721899008, - "lastWorldStats stat mean texture_memory_total": 4929813504, - "lastWorldStats stat mean visible_avatar_animator_count": 18.5627498626709, - "lastWorldStats stat mean visible_avatar_audio_source_count": 119.030822753906, - "lastWorldStats stat mean visible_avatar_bone_count": 2532.97680664063, - "lastWorldStats stat mean visible_avatar_cloth_count": 0.000216403714148328, - "lastWorldStats stat mean visible_avatar_cloth_vertices_count": 0.108634501695633, - "lastWorldStats stat mean visible_avatar_constraint_count": 41.2349243164063, - "lastWorldStats stat mean visible_avatar_constraint_depth": 175.093658447266, - "lastWorldStats stat mean visible_avatar_contact_count": 248.345596313477, - "lastWorldStats stat mean visible_avatar_light_count": 46.4164390563965, - "lastWorldStats stat mean visible_avatar_line_renderer_count": 0.0144974738359451, - "lastWorldStats stat mean visible_avatar_material_count": 419.547210693359, - "lastWorldStats stat mean visible_avatar_mesh_count": 56.8555145263672, - "lastWorldStats stat mean visible_avatar_particle_mesh_poly_count": 835112.5625, - "lastWorldStats stat mean visible_avatar_particle_system_count": 105.7841796875, - "lastWorldStats stat mean visible_avatar_particle_total_count": 31355.5546875, - "lastWorldStats stat mean visible_avatar_physbone_collider_count": 62.8025436401367, - "lastWorldStats stat mean visible_avatar_physbone_collision_count": 2001.8486328125, - "lastWorldStats stat mean visible_avatar_physbone_component_count": 207.558563232422, - "lastWorldStats stat mean visible_avatar_physbone_transform_count": 1727.359375, - "lastWorldStats stat mean visible_avatar_physics_collider_count": 4.09370899200439, - "lastWorldStats stat mean visible_avatar_physics_rigidbody_count": 2.61556100845337, - "lastWorldStats stat mean visible_avatar_poly_count": 1754423.875, - "lastWorldStats stat mean visible_avatar_skinned_mesh_count": 151.581115722656, - "lastWorldStats stat mean visible_avatar_trail_renderer_count": 2.92751169204712, - "lastWorldStats stat mean visible_player_count": 12.1258764266968, - "lastWorldStats stat min avatar_kind_Blocked": 0, - "lastWorldStats stat min avatar_kind_Custom": 14, - "lastWorldStats stat min avatar_kind_Error": 0, - "lastWorldStats stat min avatar_kind_Fallback": 0, - "lastWorldStats stat min avatar_kind_Filtered": 0, - "lastWorldStats stat min avatar_kind_Impostor": 0, - "lastWorldStats stat min avatar_kind_Loading": 0, - "lastWorldStats stat min avatar_kind_Performance": 0, - "lastWorldStats stat min avatar_kind_Safety": 0, - "lastWorldStats stat min avatar_kind_Substitute": 0, - "lastWorldStats stat min avatar_kind_Undefined": 0, - "lastWorldStats stat min avatar_perf_rating_Excellent": 0, - "lastWorldStats stat min avatar_perf_rating_Good": 2, - "lastWorldStats stat min avatar_perf_rating_Medium": 1, - "lastWorldStats stat min avatar_perf_rating_None": 0, - "lastWorldStats stat min avatar_perf_rating_Poor": 0, - "lastWorldStats stat min avatar_perf_rating_VeryPoor": 7, - "lastWorldStats stat min avatar_proxy": 0, - "lastWorldStats stat min cpu_frame_time": 0.0101669747382402, - "lastWorldStats stat min fps": 0.318867415189743, - "lastWorldStats stat min gpu_frame_time": 0, - "lastWorldStats stat min input_latency": 11.3129997253418, - "lastWorldStats stat min memory_managed_used": 420659200, - "lastWorldStats stat min ping": 50, - "lastWorldStats stat min texture_memory_current": 1712512128, - "lastWorldStats stat min texture_memory_desired": 1699394048, - "lastWorldStats stat min texture_memory_nonstreaming": 1359819520, - "lastWorldStats stat min texture_memory_saved_by_streaming": 504470016, - "lastWorldStats stat min texture_memory_total": 2226994944, - "lastWorldStats stat min visible_avatar_animator_count": 1, - "lastWorldStats stat min visible_avatar_audio_source_count": 16, - "lastWorldStats stat min visible_avatar_bone_count": 135, - "lastWorldStats stat min visible_avatar_cloth_count": 0, - "lastWorldStats stat min visible_avatar_cloth_vertices_count": 0, - "lastWorldStats stat min visible_avatar_constraint_count": 4, - "lastWorldStats stat min visible_avatar_constraint_depth": 22, - "lastWorldStats stat min visible_avatar_contact_count": 27, - "lastWorldStats stat min visible_avatar_light_count": 3, - "lastWorldStats stat min visible_avatar_line_renderer_count": 0, - "lastWorldStats stat min visible_avatar_material_count": 69, - "lastWorldStats stat min visible_avatar_mesh_count": 17, - "lastWorldStats stat min visible_avatar_particle_mesh_poly_count": 41244, - "lastWorldStats stat min visible_avatar_particle_system_count": 28, - "lastWorldStats stat min visible_avatar_particle_total_count": 4173, - "lastWorldStats stat min visible_avatar_physbone_collider_count": 6, - "lastWorldStats stat min visible_avatar_physbone_collision_count": 48, - "lastWorldStats stat min visible_avatar_physbone_component_count": 18, - "lastWorldStats stat min visible_avatar_physbone_transform_count": 75, - "lastWorldStats stat min visible_avatar_physics_collider_count": 0, - "lastWorldStats stat min visible_avatar_physics_rigidbody_count": 0, - "lastWorldStats stat min visible_avatar_poly_count": 166456, - "lastWorldStats stat min visible_avatar_skinned_mesh_count": 9, - "lastWorldStats stat min visible_avatar_trail_renderer_count": 0, - "lastWorldStats stat timeWeightedMean cpu_frame_time": 0.0237676426768303, - "lastWorldStats stat timeWeightedMean fps": 44.8868064880371, - "lastWorldStats stat timeWeightedMean gpu_frame_time": 0.00117298203986138, - "lastWorldStats stat timeWeightedMean input_latency": 24.1700096130371, - "lastWorldStats stat timeWeightedMean memory_managed_used": 790947904, - "lastWorldStats stat timeWeightedMean mutual_player_nameplates_visible": 0, - "lastWorldStats stat timeWeightedMean net_serialize_byte_count AvatarVariablesController": 57.7780342102051, - "lastWorldStats stat timeWeightedMean net_serialize_byte_count PhyBoneRecorder": 14.0104761123657, - "lastWorldStats stat timeWeightedMean net_serialize_byte_count PlayerDataStorage": 0, - "lastWorldStats stat timeWeightedMean net_serialize_byte_count PlayerStationState": 20, - "lastWorldStats stat timeWeightedMean net_serialize_byte_count SyncPhysics": 18, - "lastWorldStats stat timeWeightedMean net_serialize_byte_count UdonSync": 41.9823379516602, - "lastWorldStats stat timeWeightedMean ping": 55.9867362976074, - "lastWorldStats stat timeWeightedMean player_count": 34.9555625915527, - "lastWorldStats stat timeWeightedMean texture_memory_current": 3301012480, - "lastWorldStats stat timeWeightedMean texture_memory_desired": 2705024000, - "lastWorldStats stat timeWeightedMean texture_memory_nonstreaming": 1924121728, - "lastWorldStats stat timeWeightedMean texture_memory_saved_by_streaming": 1828674560, - "lastWorldStats stat timeWeightedMean texture_memory_total": 5129340416, - "lastWorldStats stat timeWeightedMean visible_avatar_animator_count": 18.8686294555664, - "lastWorldStats stat timeWeightedMean visible_avatar_audio_source_count": 120.888862609863, - "lastWorldStats stat timeWeightedMean visible_avatar_bone_count": 2634.51806640625, - "lastWorldStats stat timeWeightedMean visible_avatar_cloth_count": 0.000253393576713279, - "lastWorldStats stat timeWeightedMean visible_avatar_cloth_vertices_count": 0.137246757745743, - "lastWorldStats stat timeWeightedMean visible_avatar_constraint_count": 41.7905044555664, - "lastWorldStats stat timeWeightedMean visible_avatar_constraint_depth": 179.19889831543, - "lastWorldStats stat timeWeightedMean visible_avatar_contact_count": 251.879165649414, - "lastWorldStats stat timeWeightedMean visible_avatar_light_count": 46.6385459899902, - "lastWorldStats stat timeWeightedMean visible_avatar_line_renderer_count": 0.0146466158330441, - "lastWorldStats stat timeWeightedMean visible_avatar_material_count": 432.35791015625, - "lastWorldStats stat timeWeightedMean visible_avatar_mesh_count": 57.2406272888184, - "lastWorldStats stat timeWeightedMean visible_avatar_particle_mesh_poly_count": 813100.625, - "lastWorldStats stat timeWeightedMean visible_avatar_particle_system_count": 106.543190002441, - "lastWorldStats stat timeWeightedMean visible_avatar_particle_total_count": 30182.2578125, - "lastWorldStats stat timeWeightedMean visible_avatar_physbone_collider_count": 64.2963943481445, - "lastWorldStats stat timeWeightedMean visible_avatar_physbone_collision_count": 2020.67041015625, - "lastWorldStats stat timeWeightedMean visible_avatar_physbone_component_count": 216.785354614258, - "lastWorldStats stat timeWeightedMean visible_avatar_physbone_transform_count": 1802.06335449219, - "lastWorldStats stat timeWeightedMean visible_avatar_physics_collider_count": 4.0458402633667, - "lastWorldStats stat timeWeightedMean visible_avatar_physics_rigidbody_count": 2.51006436347961, - "lastWorldStats stat timeWeightedMean visible_avatar_poly_count": 1839392.875, - "lastWorldStats stat timeWeightedMean visible_avatar_skinned_mesh_count": 158.979064941406, - "lastWorldStats stat timeWeightedMean visible_avatar_trail_renderer_count": 2.91494822502136, - "lastWorldStats stat timeWeightedMean visible_player_count": 12.5415153503418, - "lastWorldStats stat timeWeightedVariance cpu_frame_time": 0.00358754186891019, - "lastWorldStats stat timeWeightedVariance fps": 89.0386505126953, - "lastWorldStats stat timeWeightedVariance gpu_frame_time": 4.59085725879049E-07, - "lastWorldStats stat timeWeightedVariance input_latency": 3572.69799804688, - "lastWorldStats stat timeWeightedVariance memory_managed_used": 1.53287415006495E+16, - "lastWorldStats stat timeWeightedVariance mutual_player_nameplates_visible": 0, - "lastWorldStats stat timeWeightedVariance ping": 3.39651393890381, - "lastWorldStats stat timeWeightedVariance player_count": 143.773468017578, - "lastWorldStats stat timeWeightedVariance texture_memory_current": 5.82160620581814E+17, - "lastWorldStats stat timeWeightedVariance texture_memory_desired": 2.73757160997913E+17, - "lastWorldStats stat timeWeightedVariance texture_memory_nonstreaming": 9.66971685800509E+16, - "lastWorldStats stat timeWeightedVariance texture_memory_saved_by_streaming": 6.61364559163949E+17, - "lastWorldStats stat timeWeightedVariance texture_memory_total": 2.29928126125808E+18, - "lastWorldStats stat timeWeightedVariance visible_avatar_animator_count": 75.9602355957031, - "lastWorldStats stat timeWeightedVariance visible_avatar_audio_source_count": 2689.11083984375, - "lastWorldStats stat timeWeightedVariance visible_avatar_bone_count": 1369580, - "lastWorldStats stat timeWeightedVariance visible_avatar_cloth_count": 0.000253324920777231, - "lastWorldStats stat timeWeightedVariance visible_avatar_cloth_vertices_count": 68.8785781860352, - "lastWorldStats stat timeWeightedVariance visible_avatar_constraint_count": 295.42041015625, - "lastWorldStats stat timeWeightedVariance visible_avatar_constraint_depth": 6237.96533203125, - "lastWorldStats stat timeWeightedVariance visible_avatar_contact_count": 8642.38671875, - "lastWorldStats stat timeWeightedVariance visible_avatar_light_count": 518.270385742188, - "lastWorldStats stat timeWeightedVariance visible_avatar_line_renderer_count": 0.0224645622074604, - "lastWorldStats stat timeWeightedVariance visible_avatar_material_count": 22843.173828125, - "lastWorldStats stat timeWeightedVariance visible_avatar_mesh_count": 921.75927734375, - "lastWorldStats stat timeWeightedVariance visible_avatar_particle_mesh_poly_count": 968052441088, - "lastWorldStats stat timeWeightedVariance visible_avatar_particle_system_count": 2699.939453125, - "lastWorldStats stat timeWeightedVariance visible_avatar_particle_total_count": 1461605760, - "lastWorldStats stat timeWeightedVariance visible_avatar_physbone_collider_count": 828.503112792969, - "lastWorldStats stat timeWeightedVariance visible_avatar_physbone_collision_count": 1496908.125, - "lastWorldStats stat timeWeightedVariance visible_avatar_physbone_component_count": 11509.9150390625, - "lastWorldStats stat timeWeightedVariance visible_avatar_physbone_transform_count": 792713.8125, - "lastWorldStats stat timeWeightedVariance visible_avatar_physics_collider_count": 10.4426460266113, - "lastWorldStats stat timeWeightedVariance visible_avatar_physics_rigidbody_count": 10.5089378356934, - "lastWorldStats stat timeWeightedVariance visible_avatar_poly_count": 763352449024, - "lastWorldStats stat timeWeightedVariance visible_avatar_skinned_mesh_count": 4880.67529296875, - "lastWorldStats stat timeWeightedVariance visible_avatar_trail_renderer_count": 19.1598815917969, - "lastWorldStats stat timeWeightedVariance visible_player_count": 17.3396167755127, - "lastWorldStats stat total net_bytes 53": 210, - "lastWorldStats stat total net_bytes avatar_interaction_list": 34076, - "lastWorldStats stat total net_bytes avatar_token": 31518, - "lastWorldStats stat total net_bytes executive_action": 10968, - "lastWorldStats stat total net_bytes frequency_request": 2371534, - "lastWorldStats stat total net_bytes instance_metadata": 251, - "lastWorldStats stat total net_bytes instantiate": 68, - "lastWorldStats stat total net_bytes ownership_transfer": 5658, - "lastWorldStats stat total net_bytes past_events": 24, - "lastWorldStats stat total net_bytes physics_serialization": 62098, - "lastWorldStats stat total net_bytes player_big_serialization": 10992261, - "lastWorldStats stat total net_bytes player_properties": 16346, - "lastWorldStats stat total net_bytes player_serialization": 16100648, - "lastWorldStats stat total net_bytes process_event": 323, - "lastWorldStats stat total net_bytes restricted_views": 39, - "lastWorldStats stat total net_bytes text_action": 1152, - "lastWorldStats stat total net_bytes udon_network_call": 336635, - "lastWorldStats stat total net_bytes udon_serialization": 129541, - "lastWorldStats stat total net_bytes udon_unreliable_serialization": 104585, - "lastWorldStats stat total net_bytes user_model_update": 46, - "lastWorldStats stat total net_bytes voice": 6236385, - "lastWorldStats stat total net_events 53": 3, - "lastWorldStats stat total net_events avatar_interaction_list": 325, - "lastWorldStats stat total net_events avatar_token": 424, - "lastWorldStats stat total net_events executive_action": 278, - "lastWorldStats stat total net_events frequency_request": 6578, - "lastWorldStats stat total net_events instance_metadata": 1, - "lastWorldStats stat total net_events instantiate": 1, - "lastWorldStats stat total net_events ownership_transfer": 196, - "lastWorldStats stat total net_events past_events": 1, - "lastWorldStats stat total net_events physics_serialization": 1018, - "lastWorldStats stat total net_events player_big_serialization": 74185, - "lastWorldStats stat total net_events player_properties": 6, - "lastWorldStats stat total net_events player_serialization": 95188, - "lastWorldStats stat total net_events process_event": 1, - "lastWorldStats stat total net_events restricted_views": 1, - "lastWorldStats stat total net_events text_action": 48, - "lastWorldStats stat total net_events udon_network_call": 8859, - "lastWorldStats stat total net_events udon_serialization": 1363, - "lastWorldStats stat total net_events udon_unreliable_serialization": 1609, - "lastWorldStats stat total net_events user_model_update": 2, - "lastWorldStats stat total net_events voice": 26779, - "lastWorldStats stat total net_serialize_byte_count AvatarVariablesController": 4302286, - "lastWorldStats stat total net_serialize_byte_count PhyBoneRecorder": 5608, - "lastWorldStats stat total net_serialize_byte_count PlayerDataStorage": 334, - "lastWorldStats stat total net_serialize_byte_count PlayerStationState": 5620, - "lastWorldStats stat total net_serialize_byte_count SyncPhysics": 50436, - "lastWorldStats stat total net_serialize_byte_count UdonSync": 73023, - "lastWorldStats stat total TweenHitch": 444308, - "lastWorldStats stat variance avatar_kind_Blocked": 0, - "lastWorldStats stat variance avatar_kind_Custom": 95.4009552001953, - "lastWorldStats stat variance avatar_kind_Error": 0, - "lastWorldStats stat variance avatar_kind_Fallback": 0, - "lastWorldStats stat variance avatar_kind_Filtered": 0, - "lastWorldStats stat variance avatar_kind_Impostor": 0, - "lastWorldStats stat variance avatar_kind_Loading": 0.165225088596344, - "lastWorldStats stat variance avatar_kind_Performance": 15.6009168624878, - "lastWorldStats stat variance avatar_kind_Safety": 0, - "lastWorldStats stat variance avatar_kind_Substitute": 0, - "lastWorldStats stat variance avatar_kind_Undefined": 0, - "lastWorldStats stat variance avatar_perf_rating_Excellent": 1.78578555583954, - "lastWorldStats stat variance avatar_perf_rating_Good": 1.55051028728485, - "lastWorldStats stat variance avatar_perf_rating_Medium": 2.81203985214233, - "lastWorldStats stat variance avatar_perf_rating_None": 13.0397977828979, - "lastWorldStats stat variance avatar_perf_rating_Poor": 1.26212823390961, - "lastWorldStats stat variance avatar_perf_rating_VeryPoor": 62.3952178955078, - "lastWorldStats stat variance avatar_proxy": 122.922508239746, - "lastWorldStats stat variance cpu_frame_time": 5.9050496929558E-05, - "lastWorldStats stat variance fps": 82.3027267456055, - "lastWorldStats stat variance gpu_frame_time": 3.09999364844771E-07, - "lastWorldStats stat variance input_latency": 58.1689071655273, - "lastWorldStats stat variance memory_managed_used": 1.60361956287447E+16, - "lastWorldStats stat variance mutual_player_nameplates_visible": 0, - "lastWorldStats stat variance ping": 3.38613367080688, - "lastWorldStats stat variance player_count": 146.619216918945, - "lastWorldStats stat variance texture_memory_current": 5.46999475676119E+17, - "lastWorldStats stat variance texture_memory_desired": 2.5321232237645E+17, - "lastWorldStats stat variance texture_memory_nonstreaming": 8.70753766851215E+16, - "lastWorldStats stat variance texture_memory_saved_by_streaming": 6.45637557157102E+17, - "lastWorldStats stat variance texture_memory_total": 2.20472807163272E+18, - "lastWorldStats stat variance visible_avatar_animator_count": 83.4059524536133, - "lastWorldStats stat variance visible_avatar_audio_source_count": 2822.19213867188, - "lastWorldStats stat variance visible_avatar_bone_count": 1323051, - "lastWorldStats stat variance visible_avatar_cloth_count": 0.000216381697100587, - "lastWorldStats stat variance visible_avatar_cloth_vertices_count": 54.5287437438965, - "lastWorldStats stat variance visible_avatar_constraint_count": 301.981231689453, - "lastWorldStats stat variance visible_avatar_constraint_depth": 6311.8310546875, - "lastWorldStats stat variance visible_avatar_contact_count": 8814.59765625, - "lastWorldStats stat variance visible_avatar_light_count": 504.415863037109, - "lastWorldStats stat variance visible_avatar_line_renderer_count": 0.0207811091095209, - "lastWorldStats stat variance visible_avatar_material_count": 21934.078125, - "lastWorldStats stat variance visible_avatar_mesh_count": 899.673095703125, - "lastWorldStats stat variance visible_avatar_particle_mesh_poly_count": 923432779776, - "lastWorldStats stat variance visible_avatar_particle_system_count": 2734.732421875, - "lastWorldStats stat variance visible_avatar_particle_total_count": 1611766656, - "lastWorldStats stat variance visible_avatar_physbone_collider_count": 796.568176269531, - "lastWorldStats stat variance visible_avatar_physbone_collision_count": 1437222.25, - "lastWorldStats stat variance visible_avatar_physbone_component_count": 10484.7587890625, - "lastWorldStats stat variance visible_avatar_physbone_transform_count": 765563, - "lastWorldStats stat variance visible_avatar_physics_collider_count": 10.6500396728516, - "lastWorldStats stat variance visible_avatar_physics_rigidbody_count": 10.6244993209839, - "lastWorldStats stat variance visible_avatar_poly_count": 706973859840, - "lastWorldStats stat variance visible_avatar_skinned_mesh_count": 4563.63916015625, - "lastWorldStats stat variance visible_avatar_trail_renderer_count": 19.4554252624512, - "lastWorldStats stat variance visible_player_count": 17.8923072814941, - "lastWorldStatusUpdatesCount": 0, - "lastWorldTags_world": [ "author_tag_furry", "author_tag_chill", "author_tag_hangout", "author_tag_avatar", "author_tag_Videoplayer", "admin_vrrat_community_takeover", "admin_community_pjkt_graffiti_grab", "system_approved", "system_monetized_world", "system_updated_recently" ], - "lastWorldTimeSpentInWorld": 9902.1640625, - "lastWorldTotalMassGiftPackageCost": 0, - "lastWorldTotalTextMessageCharacters": 0, - "lastWorldTotalTextMessagesSent": 0, - "lastWorldTotalTimeHearingSpeech": 9203.0751953125, - "lastWorldTotalTimeSpeaking": 1855.46911621094, - "lastWorldTotalTimeSpentLoadingAvatars": 861.684814453125, - "lastWorldUnityVersion": "2022.3.22f1", - "lastWorldUsersGiftedViaMassGifting": [], - "locationId": "wrld_4b341546-65ff-4607-9d38-5b7f8f405132:90179~group(grp_b6593dd3-6e86-4951-a8d8-e2fa3cb91096)~groupAccessType(public)~region(use)", - "position": "(-35.93, 108.61, 3.88)", - "source": "client", - "tags_world": [ "author_tag_furry", "author_tag_chill", "author_tag_hangout", "author_tag_avatar", "author_tag_Videoplayer", "admin_vrrat_community_takeover", "admin_community_pjkt_graffiti_grab", "system_approved", "system_monetized_world", "system_updated_recently" ], - "worldId": "wrld_4b341546-65ff-4607-9d38-5b7f8f405132", - "worldName": "Furry Hideout" - }, - "event_type": "Admin_AppClose", - "insert_id": 1815335866, - "ip": "$remote", - "language": "English", - "os_name": "Windows 11 (10.0.26200) 64bit", - "os_version": "", - "platform": "WindowsPlayer", - "session_id": 1766304295613, - "time": 1766314464634, - "user_id": "usr_ffffffff-ffff-ffff-ffff-ffffffffffff", - "user_properties": { - "acceptedTOSVersion": 12, - "accountType": "vrchat", - "avatarPerfMinRatingToDisplay": "VeryPoor", - "buildType": "steam", - "buildVersion": "2025.4.2p1-1769-54ede42845-Release", - "currentAvatarAuthorId": "usr_ffffffff-ffff-ffff-ffff-ffffffffffff", - "currentAvatarAuthorName": "Kyootfox", - "currentAvatarId": "avtr_ffffffff-ffff-ffff-ffff-ffffffffffff", - "currentAvatarName": "Fox", - "currentWorldId": "wrld_4b341546-65ff-4607-9d38-5b7f8f405132", - "currentWorldName": "Furry Hideout", - "custom_emoji_count": 0, - "custom_prop_count": 0, - "developerType": "None", - "deviceId": "ffffffffffffffffffffffffffffffffffffffff", - "displayName": "Kyootfox", - "emoji_group_count": 3, - "exclusive_emoji_count": 4, - "exclusive_prop_count": 4, - "exclusive_sticker_count": 12, - "eyeLidTrackingType": "none", - "eyeLookTrackingType": "none", - "graphicsDeviceName": "NVIDIA GeForce RTX 4070 Ti", - "graphicsDeviceVendor": "NVIDIA", - "graphicsDeviceVersion": "Direct3D 11.0 [level 11.1]", - "graphicsMemorySize": 11996, - "hudEnabled": true, - "inputMethod": "SteamVR2", - "inputType": "none", - "inVRMode": true, - "isGeForceNow": false, - "isOnMeteredConnection": false, - "joinedGroupIds": [ "grp_b6593dd3-6e86-4951-a8d8-e2fa3cb91096" ], - "joinedGroupNames": [ "The FurVerse" ], - "mainThreadPriority": "Normal", - "micEnabled": true, - "nameplatesEnabled": true, - "numberOfCustomEmoji": 0, - "numberOfFriends": 48, - "numberOfGroups": 6, - "numberOfStickersInGallery": 2, - "numExtraVRTrackers": 0, - "numOpenVRTrackers": 0, - "numOpenXRTrackers": 0, - "numOSCTrackers": 0, - "numTimesDisabledSelfieExpression": 0, - "numTimesEnabledSelfieExpression": 0, - "numTimesRefusedSelfieExpressionPopup": 0, - "numTimesRefusedSelfieExpressionPopupVrcPlus": 0, - "numTimesSelfieExpressionPopupVrcPlusClicked": 0, - "operatingSystem": "Windows 11 (10.0.26200) 64bit", - "platform": "WindowsPlayer", - "processAffinity": 0, - "processAffinityManuallySet": false, - "processorFrequency": 4192, - "processorName": "AMD Ryzen 7 7800X3D 8-Core Processor ", - "processPriority": "Normal", - "prop_group_count": 1, - "publicbetas": "default", - "releasePlatform": "Windows", - "safety_AdvancedTrustLevel1_CanSpeak": true, - "safety_AdvancedTrustLevel1_CanUseAnimatedEmoji": true, - "safety_AdvancedTrustLevel1_CanUseAvatarAudio": true, - "safety_AdvancedTrustLevel1_CanUseCustomAnimations": true, - "safety_AdvancedTrustLevel1_CanUseCustomAvatar": true, - "safety_AdvancedTrustLevel1_CanUseCustomImages": true, - "safety_AdvancedTrustLevel1_CanUseCustomShaders": true, - "safety_AdvancedTrustLevel1_CanUseDrone": true, - "safety_AdvancedTrustLevel1_CanUseParticleSystems": true, - "safety_AdvancedTrustLevel1_CanUseTriggers": true, - "safety_AdvancedTrustLevel1_CanUseUserIcons": true, - "safety_BasicTrustLevel1_CanSpeak": true, - "safety_BasicTrustLevel1_CanUseAnimatedEmoji": false, - "safety_BasicTrustLevel1_CanUseAvatarAudio": false, - "safety_BasicTrustLevel1_CanUseCustomAnimations": false, - "safety_BasicTrustLevel1_CanUseCustomAvatar": true, - "safety_BasicTrustLevel1_CanUseCustomImages": true, - "safety_BasicTrustLevel1_CanUseCustomShaders": false, - "safety_BasicTrustLevel1_CanUseDrone": true, - "safety_BasicTrustLevel1_CanUseParticleSystems": false, - "safety_BasicTrustLevel1_CanUseTriggers": false, - "safety_BasicTrustLevel1_CanUseUserIcons": true, - "safety_Friend_CanSpeak": true, - "safety_Friend_CanUseAnimatedEmoji": true, - "safety_Friend_CanUseAvatarAudio": true, - "safety_Friend_CanUseCustomAnimations": true, - "safety_Friend_CanUseCustomAvatar": true, - "safety_Friend_CanUseCustomImages": true, - "safety_Friend_CanUseCustomShaders": true, - "safety_Friend_CanUseDrone": true, - "safety_Friend_CanUseParticleSystems": true, - "safety_Friend_CanUseTriggers": true, - "safety_Friend_CanUseUserIcons": true, - "safety_IntermediateTrustLevel1_CanSpeak": true, - "safety_IntermediateTrustLevel1_CanUseAnimatedEmoji": false, - "safety_IntermediateTrustLevel1_CanUseAvatarAudio": false, - "safety_IntermediateTrustLevel1_CanUseCustomAnimations": true, - "safety_IntermediateTrustLevel1_CanUseCustomAvatar": true, - "safety_IntermediateTrustLevel1_CanUseCustomImages": true, - "safety_IntermediateTrustLevel1_CanUseCustomShaders": false, - "safety_IntermediateTrustLevel1_CanUseDrone": true, - "safety_IntermediateTrustLevel1_CanUseParticleSystems": false, - "safety_IntermediateTrustLevel1_CanUseTriggers": true, - "safety_IntermediateTrustLevel1_CanUseUserIcons": true, - "safety_IntermediateTrustLevel2_CanSpeak": true, - "safety_IntermediateTrustLevel2_CanUseAnimatedEmoji": false, - "safety_IntermediateTrustLevel2_CanUseAvatarAudio": true, - "safety_IntermediateTrustLevel2_CanUseCustomAnimations": true, - "safety_IntermediateTrustLevel2_CanUseCustomAvatar": true, - "safety_IntermediateTrustLevel2_CanUseCustomImages": true, - "safety_IntermediateTrustLevel2_CanUseCustomShaders": false, - "safety_IntermediateTrustLevel2_CanUseDrone": true, - "safety_IntermediateTrustLevel2_CanUseParticleSystems": false, - "safety_IntermediateTrustLevel2_CanUseTriggers": true, - "safety_IntermediateTrustLevel2_CanUseUserIcons": true, - "safety_Untrusted_CanSpeak": true, - "safety_Untrusted_CanUseAnimatedEmoji": false, - "safety_Untrusted_CanUseAvatarAudio": false, - "safety_Untrusted_CanUseCustomAnimations": false, - "safety_Untrusted_CanUseCustomAvatar": true, - "safety_Untrusted_CanUseCustomImages": true, - "safety_Untrusted_CanUseCustomShaders": false, - "safety_Untrusted_CanUseDrone": true, - "safety_Untrusted_CanUseParticleSystems": false, - "safety_Untrusted_CanUseTriggers": false, - "safety_Untrusted_CanUseUserIcons": true, - "safetyLevel": 5, - "safetyLevelName": "Custom", - "setting_AdvancedGraphicsAntialiasing": 2, - "setting_AFKEnabled": true, - "setting_AllowAvatarCopying": false, - "setting_AllowDirectShares": true, - "setting_AllowFocusView": true, - "setting_AllowPedestalShares": true, - "setting_AllowPrints": true, - "setting_AllowSharedConnections": true, - "setting_AllowUntrustedURL": true, - "setting_AndroidMicMode": "Mode_Normal", - "setting_ApplyColorFilterToWorld": false, - "setting_AskToPortal": true, - "setting_AutoWalkEnabled": true, - "setting_avatar_tab_start": "My Avatars", - "setting_AvatarHapticsEnabled": false, - "setting_AvatarPerformanceRatingMinimumToDisplay": "VeryPoor", - "setting_AvatarsVolume": 0.25, - "setting_AvatarVolumeEnabled": true, - "setting_BackgroundDebugLogCollection": false, - "setting_BloomIntensity": 1, - "setting_ChatBubbleAboveHeadPosition": "Above", - "setting_ChatBubbleAudioEnabled": true, - "setting_ChatBubbleAudioVolume": 0.699999988079071, - "setting_ChatBubbleAutoSend": true, - "setting_ChatBubbleOpacity": 1, - "setting_ChatBubblePositionHeight": 0.5, - "setting_ChatBubbleProfanityFilter": true, - "setting_ChatBubbleScale": 1.5, - "setting_ChatBubbleShowOwn": true, - "setting_ChatBubbleTimeout": 30, - "setting_ChatBubbleVisibility": "Everyone", - "setting_ClearCacheOnStart": false, - "setting_ClockVariant": "Boring", - "setting_ColorFilterIntensity": 0.25, - "setting_ColorFilterSelection": "NoFilter", - "setting_ComfortTunnelingMode": "Standard", - "setting_ComfortTurning": true, - "setting_CurrentLanguage": "en", - "setting_DesktopFOV": 60, - "setting_DesktopReticle": true, - "setting_DirectSharingVisibility": "Friends", - "setting_DisableAvatarCloningOnEnterWorld": true, - "setting_DisableMicButton": false, - "setting_DoubleTapMainMenuSteamVR2": true, - "setting_DownloadPrioritizeDistance": 20, - "setting_DownloadPrioritizeDistanceEnabled": true, - "setting_DownloadPrioritizeFriends": true, - "setting_DownloadPrioritizeManuallyShown": false, - "setting_DPIScaling": 50, - "setting_EarmuffMode": true, - "setting_EarmuffModeAlwaysShowVisualAid": true, - "setting_EarmuffModeAvatars": true, - "setting_EarmuffModeConeValue": 0, - "setting_EarmuffModeFalloff": 1, - "setting_EarmuffModeFollowHead": "Camera", - "setting_EarmuffModeLockRotation": false, - "setting_EarmuffModeOffsetValue": 0.550000011920929, - "setting_EarmuffModeRadius": 5.40000009536743, - "setting_EarmuffModeReducedVolume": 0.200000002980232, - "setting_EarmuffModeShowIconInNameplate": true, - "setting_FingerGrabSetting": 1, - "setting_FingerHapticSensitivity": 0.5, - "setting_FingerHapticStrength": 0.5, - "setting_FingerJumpEnabled": true, - "setting_FingerWalkSetting": 3, - "setting_FPSCapType": "Full", - "setting_FpsLimit": 30, - "setting_FPSType": "Capped", - "setting_GestureBarEnabled": false, - "setting_GraphicsQuality": "High", - "setting_GroupOnNameplate": "grp_2bd10311-6e2f-47dc-aa7b-0fb1826ad7fa", - "setting_HeaderClickScrollReset": true, - "setting_HideFallbackAvatar": false, - "setting_HideNotificationPhotos": false, - "setting_HomeAccessType": "InvitePlus", - "setting_HomeRegion": "Automatic", - "setting_HUDAnchor": "Center", - "setting_HUDMicOpacity": 0.5, - "setting_HUDMode": "Verbose", - "setting_HUDOpacity": 0.5, - "setting_HUDSmoothing": false, - "setting_InputAvatarsUseFingerTracking": true, - "setting_InputExclusiveHandTracking": true, - "setting_InputGhostHands": "AvatarHands", - "setting_InputHandsMenuOpenMode": "CircleKey", - "setting_InputShowPinchUI": false, - "setting_InteractHapticsEnabled": true, - "setting_InvertControllerVerticalLook": false, - "setting_InvertedMouse": false, - "setting_IsBoopingEnabled": true, - "setting_IsEmojiBoopEffectEnabled": true, - "setting_LandscapeFOV": 60, - "setting_LimitParticleSystems": true, - "setting_LocomotionMethod": "Gamelike", - "setting_LodQuality": "High", - "setting_LoggingEnabled": "Full", - "setting_MainMenuMovementLocked": false, - "setting_MasterVolume": 0.850000023841858, - "setting_MasterVolumeEnabled": true, - "setting_MaximumAvatarDownloadSize": 209715200, - "setting_MaximumAvatarUncompressedSize": -1, - "setting_MicBehaviorOnJoinMode": "DefaultOn", - "setting_MicDeviceName": "", - "setting_MicEnabled": true, - "setting_MicIconVisibility": "OnActivity", - "setting_MicLevel": 1, - "setting_MicMode": "Toggle", - "setting_MicToggleVolume": 0.5, - "setting_MirrorResolution": "Full", - "setting_MMFreePlacementEnabled": false, - "setting_MobileAutoHoldEnabled": true, - "setting_MobileInteractionMode": "DirectTouch", - "setting_MouseSensitivity": 1, - "setting_NameplateFallbackIconVisible": true, - "setting_NameplateMode": "Standard", - "setting_NameplateOpacity": 0.800000011920929, - "setting_NameplateQMInfo": true, - "setting_NameplateScale": "Large", - "setting_NameplateStatusMode": "ShowOnMenu", - "setting_NoiseGate": 0.00800000037997961, - "setting_NoiseSuppression": false, - "setting_NotificationsServiceEnabled": true, - "setting_OnlyShowFriendJoinLeaveAndPortalNotifications": true, - "setting_ParticlePhysicsQuality": "High", - "setting_PedestalSharingVisibility": "Everyone", - "setting_PersonalMirror.FaceMirrorOpacity": 0.699999988079071, - "setting_PersonalMirror.FaceMirrorOpacityDesktop": 1, - "setting_PersonalMirror.FaceMirrorPosX": -256.592102050781, - "setting_PersonalMirror.FaceMirrorPosXDesktop": 513.5, - "setting_PersonalMirror.FaceMirrorPosY": -31.2832946777344, - "setting_PersonalMirror.FaceMirrorPosYDesktop": 101.5, - "setting_PersonalMirror.FaceMirrorScale": 0.0935835242271423, - "setting_PersonalMirror.FaceMirrorScaleDesktop": 0.75, - "setting_PersonalMirror.FaceMirrorZoom": 0.25, - "setting_PersonalMirror.FaceMirrorZoomDesktop": 0.330000013113022, - "setting_PersonalMirror.Grabbable": true, - "setting_PersonalMirror.ImmersiveMove": false, - "setting_PersonalMirror.MirrorOpacity": 1, - "setting_PersonalMirror.MirrorScaleX": 2.10297727584839, - "setting_PersonalMirror.MirrorScaleY": 3.75877451896667, - "setting_PersonalMirror.MirrorSnapping": false, - "setting_PersonalMirror.MovementMode": 1, - "setting_PersonalMirror.ShowBorder": true, - "setting_PersonalMirror.ShowCalibrationMirror": false, - "setting_PersonalMirror.ShowEnvironmentInMirror": true, - "setting_PersonalMirror.ShowFaceMirror": true, - "setting_PersonalMirror.ShowFaceMirrorDesktop": false, - "setting_PersonalMirror.ShowRemotePlayerInMirror": true, - "setting_PersonalMirror.ShowUIInMirror": true, - "setting_PersonalSpace": false, - "setting_PixelLightCount": "High", - "setting_PlayNotificationAudio": true, - "setting_PortalModePlacement": "PlaceManually", - "setting_PortraitFOV": 80, - "setting_PreferredTimezone": "\u00cf\u00ce\u00cd\u00cf\u00cf\u00cc\u00cc\u00cd\u00cd\u00cf\u00cc\u00cf\u00cf\u00ce\u00ce\u00cc\u00ce\u00cc\u00cc\u00ce\u00cc\u00cc\u00cf", - "setting_PrintVisibility": "Everyone", - "setting_PropsVolume": 0.5, - "setting_PropsVolumeEnabled": true, - "setting_QuickSelectEnabled": false, - "setting_ReduceAnimations": false, - "setting_SafetyLevel": "Custom", - "setting_SavedCameraNearClipOverrideMode": "Off", - "setting_ScreenBrightness": 1, - "setting_ScreenContrast": 1, - "setting_SecondaryUIEnabled": false, - "setting_SelectedNetworkRegion": "Japan", - "setting_selfieExpressionEnabled": false, - "setting_ShadowQualityMode": "High", - "setting_ShowCommunityLabs": true, - "setting_ShowCommunityLabsInWorldSearch": false, - "setting_ShowCompatibilityWarnings": false, - "setting_ShowFriendRequests": true, - "setting_ShowGoButtonInLoad": true, - "setting_ShowGroupBadges": true, - "setting_ShowGroupBadgeToOthers": true, - "setting_ShowInvitesNotification": true, - "setting_ShowJoinNotifications": true, - "setting_ShowLeaveNotifications": true, - "setting_ShowPortalNotifications": false, - "setting_ShowQMDebugInfo": true, - "setting_ShowSocialRank": true, - "setting_ShowTooltips": true, - "setting_SimulateBlindness": false, - "setting_SliderSnapping": true, - "setting_StreamerModeEnabled": false, - "setting_TalkDefaultOn": true, - "setting_ThirdPersonRotation": false, - "setting_TimeFormatMode": "SystemDefault", - "setting_ToggleTalk": true, - "setting_TouchAutoRotateSpeed": 15, - "setting_TouchSensitivity": 0.117499999701977, - "setting_UIHapticsEnabled": true, - "setting_UIVolume": 1, - "setting_UIVolumeEnabled": true, - "setting_UseColorFilter": false, - "setting_UseGenericInstanceNames": true, - "setting_UseImpostorAsFallback": true, - "setting_UseOutlineMicIcon": false, - "setting_UsePixelShiftingHud": false, - "setting_UserCameraResolution": "Res_1080", - "setting_UserCameraRollWhileFlying": true, - "setting_UserCameraSaveMetadata": true, - "setting_UserCameraStreamResolution": "Res_1080", - "setting_UserCameraTriggerTakesPhotos": false, - "setting_ViveAdvanced": false, - "setting_VoicesVolume": 0.800000011920929, - "setting_VoiceVolumeEnabled": true, - "setting_WebcamDeviceName": "OBS Virtual Camera", - "setting_WingPersistenceEnabled": true, - "setting_WorldTooltipMode": "Controller", - "setting_WorldVolume": 0.349999994039536, - "setting_WorldVolumeEnabled": true, - "steamVRActualTrackingSystemName": "vrlink", - "steamVRTrackingSystemName": "oculus", - "sticker_group_count": 3, - "store": "Steam", - "subscriptionStore": "none", - "subscriptionType": "none", - "systemMemorySize": 64629, - "total_inventory_count": 23, - "uiLastMenuOpened": "QuickMenu", - "uiMainMenu.MainMenuAvatarsSelector.SelectedButton": "Recent", - "uiMainMenu.MainMenuProfileSelector.SelectedButton": "BundlesAndPacks", - "uiMainMenu.MainMenuSocialSelector.SelectedButton": "AllFriends", - "uiMainMenu.MainMenuWorldInformationSelector.SelectedButton": "Details", - "uiMainMenu.MainMenuWorldsSelector.SelectedButton": "Cell_Recents", - "uiMainMenuCurrentPage": "MainMenuSocial", - "uiMainMenuPageStack": [ "MainMenuSocial" ], - "uiQuickMenuCurrentPage": "QuickMenuSettings", - "uiQuickMenuIsAttachedToHand": true, - "uiQuickMenuIsOnRightHand": true, - "uiQuickMenuPageStack": [ "QuickMenuSettings" ], - "uiWingLeftCurrentPage": "Profile", - "uiWingLeftIsOpen": true, - "uiWingLeftPageStack": [ "Root", "Profile" ], - "uiWingRightCurrentPage": "Explore", - "uiWingRightIsOpen": true, - "uiWingRightPageStack": [ "Root", "Explore" ], - "userIcons": 1, - "vrDeviceModel": "OpenVR Headset(Oculus Quest3)", - "vrDeviceRefreshRate": 120.00479888916, - "vrDeviceRenderScale": 1 - } - } -] \ No newline at end of file diff --git a/docs/tailgrab_application.png b/docs/tailgrab_application.png new file mode 100644 index 0000000..13d6398 Binary files /dev/null and b/docs/tailgrab_application.png differ diff --git a/docs/tailgrab_tab_active_player_report_profile.png b/docs/tailgrab_tab_active_player_report_profile.png new file mode 100644 index 0000000..81699b7 Binary files /dev/null and b/docs/tailgrab_tab_active_player_report_profile.png differ diff --git a/docs/tailgrab_tab_active_players.png b/docs/tailgrab_tab_active_players.png new file mode 100644 index 0000000..13d6398 Binary files /dev/null and b/docs/tailgrab_tab_active_players.png differ diff --git a/docs/tailgrab_tab_active_players_elements.png b/docs/tailgrab_tab_active_players_elements.png new file mode 100644 index 0000000..39a0242 Binary files /dev/null and b/docs/tailgrab_tab_active_players_elements.png differ diff --git a/docs/tailgrab_tab_active_players_elements.xcf b/docs/tailgrab_tab_active_players_elements.xcf new file mode 100644 index 0000000..b375c77 Binary files /dev/null and b/docs/tailgrab_tab_active_players_elements.xcf differ diff --git a/docs/tailgrab_tab_config_avatars.png b/docs/tailgrab_tab_config_avatars.png new file mode 100644 index 0000000..d26d5cd Binary files /dev/null and b/docs/tailgrab_tab_config_avatars.png differ diff --git a/docs/tailgrab_tab_config_groups.png b/docs/tailgrab_tab_config_groups.png new file mode 100644 index 0000000..fc47ef6 Binary files /dev/null and b/docs/tailgrab_tab_config_groups.png differ diff --git a/docs/tailgrab_tab_config_linematchers.png b/docs/tailgrab_tab_config_linematchers.png new file mode 100644 index 0000000..28029e5 Binary files /dev/null and b/docs/tailgrab_tab_config_linematchers.png differ diff --git a/docs/tailgrab_tab_config_users.png b/docs/tailgrab_tab_config_users.png new file mode 100644 index 0000000..9082add Binary files /dev/null and b/docs/tailgrab_tab_config_users.png differ diff --git a/docs/tailgrab_tab_configuration.png b/docs/tailgrab_tab_configuration.png new file mode 100644 index 0000000..25489a3 Binary files /dev/null and b/docs/tailgrab_tab_configuration.png differ diff --git a/docs/tailgrab_tab_emoji_and_stickers.png b/docs/tailgrab_tab_emoji_and_stickers.png new file mode 100644 index 0000000..52fb517 Binary files /dev/null and b/docs/tailgrab_tab_emoji_and_stickers.png differ diff --git a/docs/tailgrab_tab_emoji_and_stickers_reports.png b/docs/tailgrab_tab_emoji_and_stickers_reports.png new file mode 100644 index 0000000..03fafa9 Binary files /dev/null and b/docs/tailgrab_tab_emoji_and_stickers_reports.png differ diff --git a/docs/tailgrab_tab_past_players.png b/docs/tailgrab_tab_past_players.png new file mode 100644 index 0000000..ada0640 Binary files /dev/null and b/docs/tailgrab_tab_past_players.png differ diff --git a/docs/tailgrab_tab_prints.png b/docs/tailgrab_tab_prints.png new file mode 100644 index 0000000..5e1a635 Binary files /dev/null and b/docs/tailgrab_tab_prints.png differ diff --git a/docs/tailgrab_tab_prints_report.png b/docs/tailgrab_tab_prints_report.png new file mode 100644 index 0000000..25bb6f3 Binary files /dev/null and b/docs/tailgrab_tab_prints_report.png differ diff --git a/src/AvatarManagement/AvatarManagement.cs b/src/AvatarManagement/AvatarManagement.cs index bea9128..89d8992 100644 --- a/src/AvatarManagement/AvatarManagement.cs +++ b/src/AvatarManagement/AvatarManagement.cs @@ -1,10 +1,8 @@ using ConcurrentPriorityQueue.Core; using Microsoft.EntityFrameworkCore; using NLog; -using System.Media; using Tailgrab.Clients.Ollama; using Tailgrab.Common; -using Tailgrab.Config; using Tailgrab.Models; using VRChat.API.Model; @@ -17,7 +15,12 @@ public class AvatarManagementService private ServiceRegistry _serviceRegistry; private ConcurrentPriorityQueue, int> priorityQueue = new ConcurrentPriorityQueue, int>(); + private Dictionary recentlyProcessedAvatars = new Dictionary(); + public int GetQueueCount() + { + return priorityQueue.Count; + } public AvatarManagementService(ServiceRegistry serviceRegistry) { @@ -30,7 +33,7 @@ public AvatarManagementService(ServiceRegistry serviceRegistry) public void AddAvatar(AvatarInfo avatar) { try - { + { _serviceRegistry.GetDBContext().AvatarInfos.Add(avatar); _serviceRegistry.GetDBContext().SaveChanges(); } @@ -72,121 +75,24 @@ public void DeleteAvatar(string avatarId) public void CacheAvatars(List avatarIdInCache) { TailgrabDBContext dbContext = _serviceRegistry.GetDBContext(); - - int postion = 0; foreach (var avatarId in avatarIdInCache) { EnqueueAvatarForCheck(avatarId); - AvatarInfo? dbAvatarInfo = dbContext.AvatarInfos.Find(avatarId); - bool updateNeeded = false; - if (dbAvatarInfo == null) - { - updateNeeded = true; - } - else if (!dbAvatarInfo.IsBos && - (!dbAvatarInfo.UpdatedAt.HasValue || dbAvatarInfo.UpdatedAt.Value >= DateTime.UtcNow.AddHours(-12))) - { - updateNeeded = true; - } - - if (updateNeeded) - { - Avatar? avatarData = null; - try - { - // 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 = _serviceRegistry.GetDBContext().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}"); - } - - if (avatarData == null && dbAvatarInfo == null) - { - 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}"); - } - } - } - - postion++; } } private void EnqueueAvatarForCheck(string avatarId) { + if (recentlyProcessedAvatars.TryGetValue(avatarId, out DateTime dateTime)) + { + if ((DateTime.UtcNow - dateTime).TotalMinutes < 60) + { + logger.Debug($"Skipping adding avatar {avatarId} as it was recently processed."); + return; + } + } + recentlyProcessedAvatars.Add(avatarId, DateTime.UtcNow); + var queuedItem = new QueuedAvatarProcess { AvatarId = avatarId, @@ -261,6 +167,7 @@ internal bool CheckAvatarByName(string avatarName) { string? soundSetting = ConfigStore.LoadSecret(Common.Common.Registry_Alert_Avatar) ?? "Hand"; SoundManager.PlaySound(soundSetting); + return true; } @@ -347,7 +254,7 @@ private static void CreateAvatarInfoForPrivate(TailgrabDBContext dBContext, stri } } - private static Avatar? FetchUpdateAvatarData(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, string AvatarId, AvatarInfo? dbAvatarInfo) + public static Avatar? FetchUpdateAvatarData(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, string AvatarId, AvatarInfo? dbAvatarInfo) { Avatar? avatarData = null; try diff --git a/src/Clients/Ollama/Ollama.cs b/src/Clients/Ollama/Ollama.cs index a713ccb..11e5e9c 100644 --- a/src/Clients/Ollama/Ollama.cs +++ b/src/Clients/Ollama/Ollama.cs @@ -3,11 +3,9 @@ using NLog; using OllamaSharp; using OllamaSharp.Models; -using System.Media; using System.Net.Http; using System.Text.RegularExpressions; using Tailgrab.Common; -using Tailgrab.Config; using Tailgrab.Models; using Tailgrab.PlayerManagement; using VRChat.API.Model; @@ -43,7 +41,6 @@ public class OllamaClient { public static Logger logger = LogManager.GetCurrentClassLogger(); private ConcurrentPriorityQueue, int> priorityQueue = new ConcurrentPriorityQueue, int>(); - private Dictionary processedBios = new Dictionary(); private ServiceRegistry _serviceRegistry; public OllamaClient(ServiceRegistry registry) @@ -55,7 +52,12 @@ public OllamaClient(ServiceRegistry registry) _serviceRegistry = registry; - _ = Task.Run(() => ProfileCheckTask(priorityQueue, processedBios, registry)); + _ = Task.Run(() => ProfileCheckTask(priorityQueue, registry)); + } + + public int GetQueueSize() + { + return priorityQueue.Count; } public void CheckUserProfile(string userId) @@ -78,13 +80,16 @@ 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, 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.\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); + System.Windows.MessageBox.Show("Ollama API Credentials are not set.\nThis is not nessasary for limited operation, the Profiles will not be profileText.\nOtherwise use the Config / Secrets tab to update credenials and restart Tailgrab.", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); } else { @@ -105,7 +110,7 @@ public static async Task ProfileCheckTask(ConcurrentPriorityQueue userGroups, QueuedProcess item) + private async static Task GetUserGroupInformation(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, List userGroups, QueuedProcess item) { logger.Debug($"Processing User Group subscription for userId: {item.UserId}"); bool isSuspectGroup = false; + string? watchedGroups = string.Empty; foreach (LimitedUserGroups group in userGroups) { GroupInfo? groupInfo = dBContext.GroupInfos.Find(group.GroupId); @@ -183,6 +192,7 @@ private async static void GetUserGroupInformation(ServiceRegistry serviceRegistr if (groupInfo.IsBos) { + watchedGroups = string.Concat( watchedGroups, " " + groupInfo.GroupName ); isSuspectGroup = true; } } @@ -194,21 +204,24 @@ private async static void GetUserGroupInformation(ServiceRegistry serviceRegistr if (player != null) { player.IsGroupWatch = true; - serviceRegistry.GetPlayerManager().OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); + player.PenActivity = watchedGroups; + serviceRegistry.GetPlayerManager().AddPlayerEventByUserId(item.UserId ?? string.Empty, PlayerEvent.EventType.GroupWatch, $"User is member of watched group(s): {watchedGroups}"); } string? soundSetting = ConfigStore.LoadSecret(Common.Common.Registry_Alert_Group) ?? "Hand"; SoundManager.PlaySound(soundSetting); } + + return isSuspectGroup; } - private async static void GetEvaluationFromCloud(OllamaApiClient ollamaApi, ServiceRegistry serviceRegistry, QueuedProcess item) + private async static void GetEvaluationFromCloud(OllamaApiClient ollamaApi, ServiceRegistry serviceRegistry, QueuedProcess item ) { string? ollamaPrompt = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Prompt); GenerateRequest request = new GenerateRequest { Model = ollamaApi.SelectedModel, - Prompt = ollamaPrompt ?? Tailgrab.Common.Common.Default_Ollama_API_Prompt + item.UserBio ?? string.Empty, + Prompt = string.Concat( ollamaPrompt ?? Tailgrab.Common.Common.Default_Ollama_API_Prompt, item.UserBio ?? string.Empty ), Stream = false }; @@ -234,9 +247,14 @@ await ollamaApi.GenerateAsync(request).StreamToEndAsync(responseTask => { player.UserBio = item.UserBio; player.AIEval = response; - if (IsEvaluated(player.AIEval)) + + string? profileWatch = EvaluateProfile(player.AIEval); + if (profileWatch != null) { player.IsProfileWatch = true; + player.PenActivity = $"Bio: {profileWatch}"; + serviceRegistry.GetPlayerManager().AddPlayerEventByUserId(item.UserId ?? string.Empty, PlayerEvent.EventType.ProfileWatch, $"User profile was flagged by the AI : {profileWatch}"); + serviceRegistry.GetPlayerManager().OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); } serviceRegistry.GetPlayerManager().OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); } @@ -248,26 +266,29 @@ await ollamaApi.GenerateAsync(request).StreamToEndAsync(responseTask => } } - private static void GetEvaluationFromStore(ServiceRegistry serviceRegistry, ProfileEvaluation evaluated, QueuedProcess item) + private static void GetEvaluationFromStore(ServiceRegistry serviceRegistry, ProfileEvaluation evaluated, string? userId) { - if (item.UserId != null) + if (userId != null) { - Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(item.UserId ?? string.Empty); + Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(userId ?? string.Empty); if (player != null) { player.AIEval = System.Text.Encoding.UTF8.GetString(evaluated.Evaluation); player.UserBio = System.Text.Encoding.UTF8.GetString(evaluated.ProfileText); - if (IsEvaluated(player.AIEval)) + string? profileWatch = EvaluateProfile(player.AIEval); + if (profileWatch != null) { player.IsProfileWatch = true; + player.PenActivity = profileWatch; + serviceRegistry.GetPlayerManager().AddPlayerEventByUserId(userId ?? string.Empty, PlayerEvent.EventType.ProfileWatch, $"User profile was flagged by the AI : {profileWatch}"); } serviceRegistry.GetPlayerManager().OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); - logger.Debug($"User profile already processed for userId: {item.UserId}"); + logger.Debug($"User profile already processed for userId: {userId}"); } else { - logger.Debug($"User profile lookup fails for userId: {item.UserId}"); + logger.Debug($"User profile lookup fails for userId: {userId}"); } } @@ -278,25 +299,27 @@ private static void GetEvaluationFromStore(ServiceRegistry serviceRegistry, Prof } - private static bool IsEvaluated(string? evaluated) + private static string? EvaluateProfile(string? profileText) { - if (string.IsNullOrEmpty(evaluated)) + if (string.IsNullOrEmpty(profileText)) { - return false; + return null; } - if (CheckLines(evaluated, "Explicit Sexual") || - CheckLines(evaluated, "Harrassment & Bullying") || - CheckLines(evaluated, "Self Harm")) + if (CheckLines(profileText, "Explicit Sexual")) { - - string? soundSetting = ConfigStore.LoadSecret(Common.Common.Registry_Alert_Profile) ?? "Hand"; - SoundManager.PlaySound(soundSetting); - - return true; + return "Explicit Sexual"; + } + else if (CheckLines(profileText, "Harrassment & Bullying")) + { + return "Harrassment & Bullying"; + } + else if (CheckLines(profileText, "Self Harm")) + { + return "Self Harm"; } - return false; + return null; } private static bool CheckLines(string input, string knownString) @@ -313,7 +336,110 @@ private static bool CheckLines(string input, string knownString) return firstLineContains; } + #region Image Classification + internal async Task ClassifyImageList(string userId, string assetId, List imageUrlList) + { + logger.Debug($"Classifying image from Asset: {assetId} URI: {imageUrlList.ToArray()}"); + + try + { + string? ollamaCloudKey = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Key); + if (ollamaCloudKey == null) + { + logger.Warn("Ollama API credentials are not set"); + return null; + } + + string ollamaEndpoint = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Endpoint) ?? Tailgrab.Common.Common.Default_Ollama_API_Endpoint; + + ImageReference? imageReference = await _serviceRegistry.GetVRChatAPIClient().GetImageReference(assetId, userId, imageUrlList); + if (imageReference != null) + { + ImageEvaluation? imageEvaluation = CheckImageReferenceReview(imageReference); + if (imageEvaluation == null) + { + using (HttpClient httpClient = new HttpClient()) + { + // Create Ollama client + HttpClient ollamaHttpClient = new HttpClient(); + ollamaHttpClient.BaseAddress = new Uri(ollamaEndpoint); + ollamaHttpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + ollamaCloudKey); + using (OllamaApiClient ollamaApi = new OllamaApiClient(ollamaHttpClient)) + { + string? ollamaModel = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Model) ?? Tailgrab.Common.Common.Default_Ollama_API_Model; + ollamaApi.SelectedModel = ollamaModel; + string? ollamaPrompt = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Image_Prompt); + GenerateRequest request = new GenerateRequest + { + Model = ollamaApi.SelectedModel, + Prompt = ollamaPrompt ?? Tailgrab.Common.Common.Default_Ollama_API_Image_Prompt, + Images = imageReference.Base64Data.ToArray(), + Stream = false + }; + + var response = await ollamaApi.GenerateAsync(request).StreamToEndAsync(); + + logger.Debug($"Image classified for InventoryId: {imageReference.InventoryId} as {response?.Response}"); + SaveImageEvaluation(imageReference, response?.Response); + + return response?.Response; + } + } + } + else + { + logger.Debug($"Image already classified for AssetId : {imageReference.InventoryId}"); + return System.Text.Encoding.UTF8.GetString(imageEvaluation.Evaluation); + } + } + } + catch (Exception ex) + { + logger.Error(ex, $"Error classifying image from URI: {imageUrlList.ToArray()}"); + } + + return null; + } + + private ImageEvaluation? CheckImageReferenceReview(ImageReference imageReference) + { + TailgrabDBContext dBContext = _serviceRegistry.GetDBContext(); + ImageEvaluation? evaluated = dBContext.ImageEvaluations.Find(imageReference.InventoryId); + if (evaluated != null) + { + logger.Debug($"Image already reviewed for InventoryId: {imageReference.InventoryId}"); + return evaluated; + } + return null; + } + + private void SaveImageEvaluation(ImageReference imageReference, string? response) + { + if (response != null) + { + ImageEvaluation evaluation = new ImageEvaluation + { + InventoryId = imageReference.InventoryId, + UserId = imageReference.UserId, + Md5checksum = imageReference.Md5Hash, + Evaluation = System.Text.Encoding.UTF8.GetBytes(response ?? string.Empty), + LastDateTime = DateTime.UtcNow + }; + TailgrabDBContext dBContext = _serviceRegistry.GetDBContext(); + dBContext.Add(evaluation); + dBContext.SaveChanges(); + } + } + #endregion + } + + public class ImageReference + { + public List Base64Data { get; set; } = new List(); + public string Md5Hash { get; set; } = string.Empty; + public string InventoryId { get; set; } = string.Empty; + public string UserId { get; set; } = string.Empty; } } diff --git a/src/Clients/VRChat/VRChat.cs b/src/Clients/VRChat/VRChat.cs index 31626b1..02e0a6d 100644 --- a/src/Clients/VRChat/VRChat.cs +++ b/src/Clients/VRChat/VRChat.cs @@ -3,20 +3,25 @@ using OtpNet; using System.IO; using System.Net; -using Tailgrab.Config; -using VRChat.API.Model; +using System.Net.Http; +using System.Net.Http.Json; +using Tailgrab.Clients.Ollama; +using Tailgrab.Common; using VRChat.API.Client; +using VRChat.API.Model; namespace Tailgrab.Clients.VRChat { public class VRChatClient { + private const string URI_VRC_BASE_API = "https://api.vrchat.cloud"; + public static string UserAgent = "Tailgrab/1.0.7"; public static Logger logger = LogManager.GetCurrentClassLogger(); private IVRChat? _vrchat; - public async void Initialize() + public async Task Initialize() { string? username = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_UserName); string? password = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_Password); @@ -208,6 +213,111 @@ public List GetProfileGroups(string userId) return groups; } + public async Task GetUserInventoryItem(string userId, string itemId) + { + VRChatInventoryItem? item = null; + try + { + if (_vrchat == null) + { + logger.Error("VRChat client not initialized"); + return null; + } + + string url = $"{URI_VRC_BASE_API}/api/1/user/{userId}/inventory/{itemId}"; + + // Create HTTP client with cookies + var handler = new HttpClientHandler + { + CookieContainer = new CookieContainer() + }; + + var cookies = _vrchat.GetCookies(); + foreach (var cookie in cookies) + { + handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); + } + + using var httpClient = new HttpClient(handler); + httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); + + var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + item = JsonConvert.DeserializeObject(json); + + if (item != null) + { + logger.Info($"Fetched inventory item: {item.Name} ({item.ItemType}) for user {userId}"); + } + } + catch (Exception ex) + { + logger.Error($"Error fetching inventory item {itemId} for user {userId}: {ex.Message}"); + } + + return item; + } + + public async Task GetImageReference(string inventoryId, string userId, List imageUrlList ) + { + try + { + if (_vrchat == null) + { + logger.Error("VRChat client not initialized"); + return null; + } + + // Create HTTP client with cookies + var handler = new HttpClientHandler + { + CookieContainer = new CookieContainer() + }; + + var cookies = _vrchat.GetCookies(); + foreach (var cookie in cookies) + { + handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); + } + + using HttpClient httpClient = new HttpClient(handler); + httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); + + // Download the image + string md5Hash = string.Empty; + List imageList = new List(); + int imageCount = 0; + foreach ( string imageUrl in imageUrlList) + { + byte[] contentBytes = await httpClient.GetByteArrayAsync(imageUrl); + if( imageCount == 0) + { + md5Hash = Checksum.CreateMD5(contentBytes); + } + string contentB64 = Convert.ToBase64String(contentBytes); + imageList.Add(contentB64); + } + + ImageReference iref = new ImageReference + { + Base64Data = imageList, + Md5Hash = md5Hash, + InventoryId = inventoryId, + UserId = userId + }; + + return iref; + + } + catch (Exception ex) + { + logger.Error(ex, $"Error Downloading image from URI: {imageUrlList}"); + return null; + } + } + public Print? GetPrintInfo(string fileURL) { Print? printInfo = null; @@ -242,7 +352,113 @@ public List GetProfileGroups(string userId) logger.Error($"Error fetching avatar: {ex.Message}"); } - return printInfo; + return printInfo; + } + + public List GetAvatarModerations() + { + List moderations = new List(); + try + { + if (_vrchat != null) + { + moderations = _vrchat.Authentication.GetGlobalAvatarModerations(); + } + } + catch (Exception ex) + { + logger.Error($"Error fetching avatar moderations: {ex.Message}"); + } + return moderations; + } + + public async Task BlockAvatarGlobal(string avatarId) + { + try + { + if (_vrchat == null) + { + logger.Info($"Failed Block avatar {avatarId} globally, not logged in."); + return false; + } + + // Create HTTP client with cookies + var handler = new HttpClientHandler + { + CookieContainer = new CookieContainer() + }; + + var cookies = _vrchat.GetCookies(); + foreach (var cookie in cookies) + { + handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); + } + + using HttpClient httpClient = new HttpClient(handler); + httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); + + AvatarModerationItem rpt = new AvatarModerationItem + { + TargetAvatarId = avatarId, + AvatarModerationType = "block" + }; + + HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{URI_VRC_BASE_API}/api/1/auth/user/avatarmoderations?targetAvatarId={avatarId}&avatarModerationType=block", rpt); + string responseContent = await response.Content.ReadAsStringAsync(); + logger.Debug($"Response from Block avatar {avatarId} globally: {responseContent}"); + logger.Info($"Submitted Block avatar {avatarId} globally."); + response.EnsureSuccessStatusCode(); + + return response.IsSuccessStatusCode; + } + catch (Exception ex) + { + logger.Error($"Error setting avatar moderation status: {ex.Message}"); + } + + return false; + } + + + public async Task DeleteAvatarGlobal(string avatarId) + { + try + { + if (_vrchat == null) + { + logger.Info($"Failed Unblock avatar {avatarId} globally, not logged in."); + return false; + } + + // Create HTTP client with cookies + var handler = new HttpClientHandler + { + CookieContainer = new CookieContainer() + }; + + var cookies = _vrchat.GetCookies(); + foreach (var cookie in cookies) + { + handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); + } + + using HttpClient httpClient = new HttpClient(handler); + httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); + + HttpResponseMessage response = await httpClient.DeleteAsync($"{URI_VRC_BASE_API}/api/1/auth/user/avatarmoderations?targetAvatarId={avatarId}&avatarModerationType=block"); + string responseContent = await response.Content.ReadAsStringAsync(); + logger.Debug($"Response from Block avatar {avatarId} globally: {responseContent}"); + logger.Info($"Submitted Block avatar {avatarId} globally."); + response.EnsureSuccessStatusCode(); + + return response.IsSuccessStatusCode; + } + catch (Exception ex) + { + logger.Error($"Error setting avatar moderation status: {ex.Message}"); + } + + return false; } @@ -271,6 +487,201 @@ private static void SaveCookiesToFile(string filePath, List cookies) return group; } + internal async Task SubmitModerationReportAsync(ModerationReportPayload rpt) + { + try + { + if (_vrchat == null) + { + logger.Error("VRChat client not initialized"); + return false; + } + + // Create HTTP client with cookies + var handler = new HttpClientHandler + { + CookieContainer = new CookieContainer() + }; + + var cookies = _vrchat.GetCookies(); + foreach (var cookie in cookies) + { + handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); + } + + using HttpClient httpClient = new HttpClient(handler); + httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); + + // Download the image + HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{URI_VRC_BASE_API}/api/1/moderationReports", rpt); + string responseContent = await response.Content.ReadAsStringAsync(); + logger.Debug($"Response from submitting moderation report for content {rpt.ContentId}: {responseContent}"); + logger.Info($"Submitted moderation report for content {rpt.ContentId} with reason: {rpt.Reason}\n{responseContent}"); + response.EnsureSuccessStatusCode(); + + return response.IsSuccessStatusCode; + + } + catch (Exception ex) + { + logger.Error(ex, $"Error Reporting image from URI: {rpt}"); + return false; + } + } + + public class AvatarModerationItem + { + [JsonProperty("avatarModerationType")] + public string AvatarModerationType { get; set; } = "block"; + + [JsonProperty("targetAvatarId")] + public string TargetAvatarId { get; set; } = string.Empty; + } + + public class VRChatInventoryItem + { + [JsonProperty("id")] + public string Id { get; set; } = string.Empty; + + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("description")] + public string Description { get; set; } = string.Empty; + + [JsonProperty("itemType")] + public string ItemType { get; set; } = string.Empty; + + [JsonProperty("itemTypeLabel")] + public string ItemTypeLabel { get; set; } = string.Empty; + + [JsonProperty("imageUrl")] + public string ImageUrl { get; set; } = string.Empty; + + [JsonProperty("holderId")] + public string HolderId { get; set; } = string.Empty; + + [JsonProperty("ancestor")] + public string Ancestor { get; set; } = string.Empty; + + [JsonProperty("ancestorHolderId")] + public string AncestorHolderId { get; set; } = string.Empty; + + [JsonProperty("firstAncestor")] + public string FirstAncestor { get; set; } = string.Empty; + + [JsonProperty("firstAncestorHolderId")] + public string FirstAncestorHolderId { get; set; } = string.Empty; + + [JsonProperty("collections")] + public List Collections { get; set; } = new List(); + + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; set; } + + [JsonProperty("template_created_at")] + public DateTime TemplateCreatedAt { get; set; } + + [JsonProperty("template_updated_at")] + public DateTime TemplateUpdatedAt { get; set; } + + [JsonProperty("defaultAttributes")] + public Dictionary DefaultAttributes { get; set; } = new Dictionary(); + + [JsonProperty("userAttributes")] + public Dictionary UserAttributes { get; set; } = new Dictionary(); + + [JsonProperty("equipSlot")] + public string EquipSlot { get; set; } = string.Empty; + + [JsonProperty("equipSlots")] + public List EquipSlots { get; set; } = new List(); + + [JsonProperty("expiryDate")] + public DateTime? ExpiryDate { get; set; } + + [JsonProperty("flags")] + public List Flags { get; set; } = new List(); + + [JsonProperty("isArchived")] + public bool IsArchived { get; set; } + + [JsonProperty("isSeen")] + public bool IsSeen { get; set; } + + [JsonProperty("metadata")] + public InventoryItemMetadata? Metadata { get; set; } + + [JsonProperty("quantifiable")] + public bool Quantifiable { get; set; } + + [JsonProperty("tags")] + public List Tags { get; set; } = new List(); + + [JsonProperty("templateId")] + public string TemplateId { get; set; } = string.Empty; + + [JsonProperty("validateUserAttributes")] + public bool ValidateUserAttributes { get; set; } + } + + public class InventoryItemMetadata + { + [JsonProperty("animated")] + public bool Animated { get; set; } + + [JsonProperty("animationStyle")] + public string AnimationStyle { get; set; } = string.Empty; + + [JsonProperty("fileId")] + public string FileId { get; set; } = string.Empty; + + [JsonProperty("imageUrl")] + public string ImageUrl { get; set; } = string.Empty; + + [JsonProperty("maskTag")] + public string MaskTag { get; set; } = string.Empty; + } + + public class ModerationReportPayload + { + [JsonProperty("type")] + public string Type { get; set; } = string.Empty; + + [JsonProperty("category")] + public string Category { get; set; } = string.Empty; + + [JsonProperty("reason")] + public string Reason { get; set; } = string.Empty; + + [JsonProperty("contentId")] + public string ContentId { get; set; } = string.Empty; + + [JsonProperty("description")] + public string Description { get; set; } = string.Empty; + + [JsonProperty("details")] + public List Details { get; set; } = new List(); + } + + public class ModerationReportDetails + { + [JsonProperty("instanceType")] + public string InstanceType { get; set; } = string.Empty; + + [JsonProperty("instanceAgeGated")] + public bool InstanceAgeGated { get; set; } + + [JsonProperty("userInSameInstance")] + public bool UserInSameInstance { get; set; } = true; + + [JsonProperty("holderId")] + public string HolderId { get; set; } = string.Empty; + } + private class SerializableCookie { public string Name { get; set; } = string.Empty; @@ -311,5 +722,37 @@ public static SerializableCookie FromCookie(Cookie c) }; } } + + public class PrintInfo + { + [JsonProperty("authorId")] + public string AuthorId { get; set; } = string.Empty; + [JsonProperty("authorName")] + public string AuthorName { get; set; } = string.Empty; + [JsonProperty("id")] + public string Id { get; set; } = string.Empty; + [JsonProperty("createdAt")] + public string CreatedAt { get; set; } = string.Empty; + [JsonProperty("note")] + public string Note { get; set; } = string.Empty; + [JsonProperty("ownerId")] + public string OwnerId { get; set; } = string.Empty; + [JsonProperty("timestamp")] + public string Timestamp { get; set; } = string.Empty; + [JsonProperty("worldId")] + public string WorldId { get; set; } = string.Empty; + [JsonProperty("worldName")] + public string WorldName { get; set; } = string.Empty; + [JsonProperty("files")] + public PrintFileInfo FileInfo { get; set; } = new PrintFileInfo(); + } + + public class PrintFileInfo + { + [JsonProperty("fileId")] + public string FileId { get; set; } = string.Empty; + [JsonProperty("image")] + public string ImageUrl { get; set; } = string.Empty; + } } } diff --git a/src/Common/Checksum.cs b/src/Common/Checksum.cs index e956cc6..a349e45 100644 --- a/src/Common/Checksum.cs +++ b/src/Common/Checksum.cs @@ -10,15 +10,17 @@ public static string CreateMD5(string input) byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input); byte[] hashBytes = md5.ComputeHash(inputBytes); - return Convert.ToHexString(hashBytes); // .NET 5 + + return Convert.ToHexString(hashBytes); + } + } + public static string CreateMD5(byte[] imageBytes) + { + // Use input string to calculate MD5 hash + using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) + { + byte[] hashBytes = md5.ComputeHash(imageBytes); - // Convert the byte array to hexadecimal string prior to .NET 5 - // StringBuilder sb = new System.Text.StringBuilder(); - // for (int i = 0; i < hashBytes.Length; i++) - // { - // sb.Append(hashBytes[i].ToString("X2")); - // } - // return sb.ToString(); + return Convert.ToHexString(hashBytes); } } } diff --git a/src/Common/Common.cs b/src/Common/Common.cs index 2ffc066..77b2438 100644 --- a/src/Common/Common.cs +++ b/src/Common/Common.cs @@ -15,8 +15,10 @@ public static class Common 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"; + public const string Registry_Ollama_API_Image_Prompt = "OLLAMA_API_PROMPT_IMAGE"; public const string Registry_Ollama_API_Model = "OLLAMA_API_Model"; - public const string Default_Ollama_API_Prompt = "From the following block of text, classify the contents into a single class;\n'OK', 'Explicit Sexual', 'Harrassment & Bullying', 'Self Harm' or 'Other'.\nWhen replying, give a single line for the Classification and then a new line for the resoning: \n"; + public const string Default_Ollama_API_Prompt = "From the following block of text, classify the contents into a single class from the following classes;\r\n'OK' - Where as all text content can be considered PG13;\r\n'Explicit Sexual' - Where as any of the text contained describes sexual acts or intent. Flagged words Bussy, Fagot, Dih;\r\n'Harassment & Bullying' - Where the text is describing acts of trolling or bullying users on Religion, Sexual Orientation or Race. Flagged words of base nigg* and variations of that spelling to hide racism.\r\n'Self Harm' - Any part of the text where it explicitly describes destructive behaviours.\r\nIf there is not enough information to determine the class, use a default of OK. When replying, return a single line for the Classification and a carriage return, then place the reasoning on subsequent lines, translate any foreign language to English: \n"; + public const string Default_Ollama_API_Image_Prompt = "From the attached image, reply with a single classification of, 'OK', 'Sexual Content', 'Gore' or 'Racism'. In the following line, give the reasoning for the classifcation."; public const string Default_Ollama_API_Endpoint = "https://ollama.com"; public const string Default_Ollama_API_Model = "gemma3:27b"; @@ -25,5 +27,12 @@ public static class Common public const string Registry_Alert_Group = "ALERT_GROUP_SOUND"; public const string Registry_Alert_Profile = "ALERT_PROFILE_SOUND"; + // Gist related registry keys + public const string Registry_Group_Checksum = "GIST_GROUP_LIST_CHECKSUM"; + public const string Registry_Group_Gist = "GIST_GROUP_LIST_URL"; + + // Avatar Gist related registry keys + public const string Registry_Avatar_Checksum = "GIST_AVATAR_LIST_CHECKSUM"; + public const string Registry_Avatar_Gist = "GIST_AVATAR_LIST_URL"; } } diff --git a/src/Common/ConfigStore.cs b/src/Common/ConfigStore.cs new file mode 100644 index 0000000..9aa263c --- /dev/null +++ b/src/Common/ConfigStore.cs @@ -0,0 +1,102 @@ +using Microsoft.Win32; +using NLog; +using System.Security.Cryptography; +using System.Text; + +namespace Tailgrab.Common +{ + public static class ConfigStore + { + public static Logger logger = LogManager.GetCurrentClassLogger(); + + public static void SaveSecret(string name, string value) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (value == null) value = string.Empty; + + var bytes = Encoding.UTF8.GetBytes(value); + var protectedBytes = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser); + var base64 = Convert.ToBase64String(protectedBytes); + + using (var key = Registry.CurrentUser.CreateSubKey(Common.ConfigRegistryPath)) + { + key.SetValue(name, base64, RegistryValueKind.String); + } + } + + public static string? LoadSecret(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + using (var key = Registry.CurrentUser.OpenSubKey(Common.ConfigRegistryPath)) + { + if (key == null) return null; + var base64 = key.GetValue(name) as string; + if (string.IsNullOrEmpty(base64)) return null; + try + { + var protectedBytes = Convert.FromBase64String(base64); + var bytes = ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser); + return Encoding.UTF8.GetString(bytes); + } + catch + { + return null; + } + } + } + + public static void DeleteSecret(string name) + { + using (var key = Registry.CurrentUser.OpenSubKey(Common.ConfigRegistryPath, writable: true)) + { + if (key == null) return; + key.DeleteValue(name, throwOnMissingValue: false); + } + } + + public static string? GetStoredUri(string keyName) + { + try + { + using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(Common.ConfigRegistryPath)) + { + if (key == null) + { + logger.Debug($"Registry key does not exist {keyName}"); + return null; + } + + string? value = key.GetValue(keyName) as string; + if (string.IsNullOrEmpty(value)) + { + logger.Debug($"No Value stored in registry. {keyName}"); + return null; + } + + return value; + } + } + catch (Exception ex) + { + logger.Error(ex, "Failed to read value from registry."); + return null; + } + } + + public static void PutStoredUri(string keyName, string keyValue) + { + try + { + using (RegistryKey key = Registry.CurrentUser.CreateSubKey(Common.ConfigRegistryPath)) + { + key.SetValue(keyName, keyValue, RegistryValueKind.String); + } + } + catch (Exception ex) + { + logger.Error(ex, $"Failed to save value to registry. {keyName}"); + } + } + } +} diff --git a/src/Config/ConfigStore.cs b/src/Config/ConfigStore.cs deleted file mode 100644 index 87d543f..0000000 --- a/src/Config/ConfigStore.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Microsoft.Win32; -using System.Security.Cryptography; -using System.Text; - -namespace Tailgrab.Config -{ - public static class ConfigStore - { - private const string RegistryPath = "Software\\DeviousFox\\Tailgrab\\Config"; - - public static void SaveSecret(string name, string value) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (value == null) value = string.Empty; - - var bytes = Encoding.UTF8.GetBytes(value); - var protectedBytes = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser); - var base64 = Convert.ToBase64String(protectedBytes); - - using (var key = Registry.CurrentUser.CreateSubKey(RegistryPath)) - { - key.SetValue(name, base64, RegistryValueKind.String); - } - } - - public static string? LoadSecret(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - using (var key = Registry.CurrentUser.OpenSubKey(RegistryPath)) - { - if (key == null) return null; - var base64 = key.GetValue(name) as string; - if (string.IsNullOrEmpty(base64)) return null; - try - { - var protectedBytes = Convert.FromBase64String(base64); - var bytes = ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser); - return Encoding.UTF8.GetString(bytes); - } - catch - { - return null; - } - } - } - - public static void DeleteSecret(string name) - { - using (var key = Registry.CurrentUser.OpenSubKey(RegistryPath, writable: true)) - { - if (key == null) return; - key.DeleteValue(name, throwOnMissingValue: false); - } - } - } -} diff --git a/src/LineHandlers/AvatarChangeHandler.cs b/src/LineHandlers/AvatarChangeHandler.cs index 1da722c..44366d0 100644 --- a/src/LineHandlers/AvatarChangeHandler.cs +++ b/src/LineHandlers/AvatarChangeHandler.cs @@ -33,7 +33,6 @@ public override bool HandleLine(string line) } _serviceRegistry.GetPlayerManager().SetAvatarForPlayer(userName, avatarName); - _serviceRegistry.GetPlayerManager().AddPlayerEventByDisplayName(userName, PlayerEvent.EventType.AvatarChange, $"Changed avatar to: {avatarName}"); ExecuteActions(); return true; diff --git a/src/LineHandlers/EmojiHandler.cs b/src/LineHandlers/EmojiHandler.cs index 0ac4bb7..3dc47f6 100644 --- a/src/LineHandlers/EmojiHandler.cs +++ b/src/LineHandlers/EmojiHandler.cs @@ -6,15 +6,16 @@ namespace Tailgrab.LineHandler; public class EmojiHandler : AbstractLineHandler { - public static readonly string LOG_PATTERN = @"([\d]{4}.[\d]{2}.[\d]{2}\W[\d]{2}:[\d]{2}:[\d]{2})\W(Log[\W]{8}|Debug[\W]{6})-\W\W\[API\]\W\[\d+\]\WSending\WGet\Wrequest\Wto\Whttps://api.vrchat.cloud/api/1/inventory/spawn\?id=([\d\w\W]+)"; + public static readonly string LOG_PATTERN = @"([\d]{4}.[\d]{2}.[\d]{2}\W[\d]{2}:[\d]{2}:[\d]{2})\W(Log[\W]{8}|Debug[\W]{6})-\W\W\[API\]\W\[\d+\]\WSending\WGet\Wrequest\Wto\Whttps://api.vrchat.cloud/api/1/user/(usr_[\d\w\W]+)/inventory/(inv_[\d\w\W]+)"; public static readonly int VRC_DATETIME = 1; public static readonly int VRC_LOGTYPE = 2; - public static readonly int VRC_FILEURL = 3; + public static readonly int VRC_USERID = 3; + public static readonly int VRC_INVENTORYID = 4; public EmojiHandler(string matchPattern, ServiceRegistry serviceRegistry) : base(matchPattern, serviceRegistry) { - logger.Info($"** Emoji Handler: Regular Expression: {Pattern}"); + logger.Info($"** Emoji/Inventory Handler: Regular Expression: {Pattern}"); } public override bool HandleLine(string line) @@ -23,11 +24,12 @@ public override bool HandleLine(string line) if (m.Success) { string timestamp = m.Groups[VRC_DATETIME].Value; - string fileURL = m.Groups[VRC_FILEURL].Value; - _serviceRegistry.GetPlayerManager().AddInventorySpawn(fileURL); + string userId = m.Groups[VRC_USERID].Value; + string inventoryId = m.Groups[VRC_INVENTORYID].Value; + _serviceRegistry.GetPlayerManager().AddInventorySpawn(userId, inventoryId); if (LogOutput) { - logger.Info($"{COLOR_PREFIX}Print : {fileURL}{COLOR_RESET.GetAnsiEscape()}"); + logger.Info($"{COLOR_PREFIX}Emoji/Inventory : {userId} / {inventoryId}{COLOR_RESET.GetAnsiEscape()}"); } ExecuteActions(); return true; diff --git a/src/Models/ImageEvaluation.cs b/src/Models/ImageEvaluation.cs new file mode 100644 index 0000000..eb67bff --- /dev/null +++ b/src/Models/ImageEvaluation.cs @@ -0,0 +1,26 @@ +// This file has been auto generated by EF Core Power Tools. +#nullable disable +using Microsoft.AspNetCore.Http.HttpResults; +using System; +using System.Collections.Generic; + +namespace Tailgrab.Models; + +public partial class ImageEvaluation +{ + public string InventoryId { get; set; } + public string UserId { get; set; } + public string Md5checksum { get; set; } + public byte[] Evaluation { get; set; } + public DateTime LastDateTime { get; set; } + + public ImageEvaluation() + { + LastDateTime = DateTime.UtcNow; + } + + public override string ToString() + { + return $"InventoryId: {InventoryId}, UserId: {UserId}, Md5checksum: {Md5checksum}, Evaluation: {BitConverter.ToString(Evaluation)}, LastDateTime: {LastDateTime}"; + } +} \ No newline at end of file diff --git a/src/Models/TailgrabDBContext.cs b/src/Models/TailgrabDBContext.cs index 1b15339..9c70505 100644 --- a/src/Models/TailgrabDBContext.cs +++ b/src/Models/TailgrabDBContext.cs @@ -1,12 +1,25 @@ // This file has been auto generated by EF Core Power Tools. #nullable disable using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; namespace Tailgrab.Models; + +public class TailgrabContextFactory : IDesignTimeDbContextFactory +{ + public TailgrabDBContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite("Data Source=./Resources/tailgrab-dev.sqlite"); + + return new TailgrabDBContext(optionsBuilder.Options); + } +} + public partial class TailgrabDBContext : DbContext { public TailgrabDBContext(DbContextOptions options) @@ -25,6 +38,8 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) public virtual DbSet ProfileEvaluations { get; set; } + public virtual DbSet ImageEvaluations { get; set; } + public virtual DbSet UserInfos { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -69,6 +84,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.IsBos).HasColumnName("IsBOS"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.InventoryId); + + entity.ToTable("ImageEvaluation"); + + entity.Property(e => e.Md5checksum).HasColumnName("MD5Checksum"); + }); + OnModelCreatingPartial(modelBuilder); } diff --git a/src/NLog.config b/src/NLog.config index 97c860e..b15b72e 100644 --- a/src/NLog.config +++ b/src/NLog.config @@ -12,7 +12,6 @@ - - + \ No newline at end of file diff --git a/src/PlayerManagement/AvatarCollection.cs b/src/PlayerManagement/AvatarCollection.cs new file mode 100644 index 0000000..a471ece --- /dev/null +++ b/src/PlayerManagement/AvatarCollection.cs @@ -0,0 +1,194 @@ +using System.Collections.Specialized; +using System.ComponentModel; +using Microsoft.EntityFrameworkCore; + +namespace Tailgrab.PlayerManagement +{ + // Lightweight virtualizing collection for Avatar DB. It only fetches items on demand + // and holds a small cache to limit memory usage. It queries the EF DB context for + // counts and pages of avatars ordered by AvatarName. + public class AvatarVirtualizingCollection : System.Collections.IList, System.Collections.IEnumerable, System.Collections.Specialized.INotifyCollectionChanged + { + private readonly ServiceRegistry _services; + private readonly int _pageSize = 100; + private readonly Dictionary> _pages = new Dictionary>(); + private int _count = -1; + private string? _filterText; + + public AvatarVirtualizingCollection(ServiceRegistry services) + { + _services = services; + } + + public void SetFilter(string? filterText) + { + if (_filterText != filterText) + { + _filterText = filterText; + Refresh(); + } + } + + public void Refresh() + { + _pages.Clear(); + _count = -1; + CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + private void EnsureCount() + { + if (_count >= 0) return; + try + { + var db = _services.GetDBContext(); + var query = db.AvatarInfos.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(_filterText)) + { + if (_filterText.StartsWith("avtr_", StringComparison.OrdinalIgnoreCase)) + { + query = query.Where(a => a.AvatarId == _filterText); + } + else + { + query = query.Where(a => EF.Functions.Like(a.AvatarName, $"%{_filterText}%")); + } + } + + _count = query.Count(); + } + catch + { + _count = 0; + } + } + + private AvatarInfoViewModel? LoadAtIndex(int index) + { + if (index < 0) return null; + EnsureCount(); + if (index >= _count) return null; + var page = index / _pageSize; + if (!_pages.TryGetValue(page, out var list)) + { + // load this page + try + { + var db = _services.GetDBContext(); + var skip = page * _pageSize; + var query = db.AvatarInfos.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(_filterText)) + { + var filterLower = _filterText.ToLower(); + query = query.Where(a => a.AvatarName.ToLower().Contains(filterLower)); + } + + var items = query.OrderBy(a => a.AvatarName).Skip(skip).Take(_pageSize).ToList(); + list = items.Select(a => new AvatarInfoViewModel(a)).ToList(); + _pages[page] = list; + // Keep only a couple pages in memory (current, prev, next) + var keep = new HashSet { page, page - 1, page + 1 }; + var keys = _pages.Keys.ToList(); + foreach (var k in keys) + { + if (!keep.Contains(k)) _pages.Remove(k); + } + } + catch + { + list = new List(); + } + } + var idxInPage = index % _pageSize; + if (idxInPage < list.Count) return list[idxInPage]; + return null; + } + + // IList implementation (read-only for UI) + public int Add(object? value) => throw new NotSupportedException(); + public void Clear() => throw new NotSupportedException(); + public bool Contains(object? value) + { + EnsureCount(); + if (value is AvatarInfoViewModel vm) return this.Cast().Any(x => x.AvatarId == vm.AvatarId); + return false; + } + public int IndexOf(object? value) => -1; + public void Insert(int index, object? value) => throw new NotSupportedException(); + public void Remove(object? value) => throw new NotSupportedException(); + public void RemoveAt(int index) => throw new NotSupportedException(); + public bool IsReadOnly => true; + public bool IsFixedSize => false; + public object? this[int index] + { + get { return LoadAtIndex(index); } + set => throw new NotSupportedException(); + } + + public void CopyTo(Array array, int index) + { + EnsureCount(); + for (int i = 0; i < _count; i++) array.SetValue(LoadAtIndex(i), index + i); + } + + public int Count + { + get { EnsureCount(); return _count; } + } + + public bool IsSynchronized => false; + public object SyncRoot => this; + public System.Collections.IEnumerator GetEnumerator() + { + EnsureCount(); + for (int i = 0; i < _count; i++) yield return LoadAtIndex(i)!; + } + + // Collection changed event for WPF to react to resets + public event NotifyCollectionChangedEventHandler? CollectionChanged; + } + + public class AvatarInfoViewModel : INotifyPropertyChanged + { + public string AvatarId { get; set; } + public string AvatarName { get; set; } + private bool _isBos; + public bool IsBos + { + get => _isBos; + set + { + if (_isBos != value) + { + _isBos = value; + IsBosText = BoolToYesNo(_isBos); + OnPropertyChanged(nameof(IsBos)); + OnPropertyChanged(nameof(IsBosText)); + } + } + } + + public string IsBosText { get; set; } + public DateTime? UpdatedAt { get; set; } + + public AvatarInfoViewModel(Tailgrab.Models.AvatarInfo a) + { + AvatarId = a.AvatarId; + AvatarName = a.AvatarName; + IsBos = a.IsBos; + UpdatedAt = a.UpdatedAt; + IsBosText = BoolToYesNo(IsBos); + } + + // Convert boolean to YES/NO string for display + public static string BoolToYesNo(bool value) => value ? "YES" : "NO"; + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/PlayerManagement/GroupCollection.cs b/src/PlayerManagement/GroupCollection.cs new file mode 100644 index 0000000..efb9bbd --- /dev/null +++ b/src/PlayerManagement/GroupCollection.cs @@ -0,0 +1,195 @@ +using System.Collections.Specialized; +using System.ComponentModel; +using Microsoft.EntityFrameworkCore; + +namespace Tailgrab.PlayerManagement +{ + // Virtualizing collection for Groups similar to AvatarVirtualizingCollection + public class GroupVirtualizingCollection : System.Collections.IList, System.Collections.IEnumerable, System.Collections.Specialized.INotifyCollectionChanged + { + private readonly ServiceRegistry _services; + private readonly int _pageSize = 100; + private readonly Dictionary> _pages = new Dictionary>(); + private int _count = -1; + private string? _filterText; + + public GroupVirtualizingCollection(ServiceRegistry services) + { + _services = services; + } + + public void SetFilter(string? filterText) + { + if (_filterText != filterText) + { + _filterText = filterText; + Refresh(); + } + } + + public void Refresh() + { + _pages.Clear(); + _count = -1; + CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + private void EnsureCount() + { + if (_count >= 0) return; + try + { + var db = _services.GetDBContext(); + var query = db.GroupInfos.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(_filterText)) + { + if (_filterText.StartsWith("grp_", StringComparison.OrdinalIgnoreCase)) + { + query = query.Where(g => g.GroupId == _filterText); + } + else + { + query = query.Where(g => EF.Functions.Like(g.GroupName, $"%{_filterText}%")); + } + } + + _count = query.Count(); + } + catch + { + _count = 0; + } + } + + private GroupInfoViewModel? LoadAtIndex(int index) + { + if (index < 0) return null; + EnsureCount(); + if (index >= _count) return null; + var page = index / _pageSize; + if (!_pages.TryGetValue(page, out var list)) + { + try + { + var db = _services.GetDBContext(); + var skip = page * _pageSize; + var query = db.GroupInfos.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(_filterText)) + { + if (_filterText.StartsWith("grp_", StringComparison.OrdinalIgnoreCase)) + { + query = query.Where(g => g.GroupId == _filterText); + } + else + { + query = query.Where(g => EF.Functions.Like(g.GroupName, $"%{_filterText}%")); + } + } + + var items = query.OrderBy(a => a.GroupName).Skip(skip).Take(_pageSize).ToList(); + list = items.Select(a => new GroupInfoViewModel(a)).ToList(); + _pages[page] = list; + var keep = new HashSet { page, page - 1, page + 1 }; + var keys = _pages.Keys.ToList(); + foreach (var k in keys) + { + if (!keep.Contains(k)) _pages.Remove(k); + } + } + catch + { + list = new List(); + } + } + var idxInPage = index % _pageSize; + if (idxInPage < list.Count) return list[idxInPage]; + return null; + } + + // IList implementation (read-only) + public int Add(object? value) => throw new NotSupportedException(); + public void Clear() => throw new NotSupportedException(); + public bool Contains(object? value) + { + EnsureCount(); + if (value is GroupInfoViewModel vm) return this.Cast().Any(x => x.GroupId == vm.GroupId); + return false; + } + public int IndexOf(object? value) => -1; + public void Insert(int index, object? value) => throw new NotSupportedException(); + public void Remove(object? value) => throw new NotSupportedException(); + public void RemoveAt(int index) => throw new NotSupportedException(); + public bool IsReadOnly => true; + public bool IsFixedSize => false; + public object? this[int index] + { + get { return LoadAtIndex(index); } + set => throw new NotSupportedException(); + } + + public void CopyTo(Array array, int index) + { + EnsureCount(); + for (int i = 0; i < _count; i++) array.SetValue(LoadAtIndex(i), index + i); + } + + public int Count + { + get { EnsureCount(); return _count; } + } + + public bool IsSynchronized => false; + public object SyncRoot => this; + public System.Collections.IEnumerator GetEnumerator() + { + EnsureCount(); + for (int i = 0; i < _count; i++) yield return LoadAtIndex(i)!; + } + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + } + + public class GroupInfoViewModel : INotifyPropertyChanged + { + public string GroupId { get; set; } + public string GroupName { get; set; } + private bool _isBos; + public bool IsBos + { + get => _isBos; + set + { + if (_isBos != value) + { + _isBos = value; + IsBosText = BoolToYesNo(_isBos); + OnPropertyChanged(nameof(IsBos)); + OnPropertyChanged(nameof(IsBosText)); + } + } + } + + public string IsBosText { get; set; } + public DateTime? UpdatedAt { get; set; } + + public GroupInfoViewModel(Tailgrab.Models.GroupInfo a) + { + GroupId = a.GroupId; + GroupName = a.GroupName; + IsBos = a.IsBos; + UpdatedAt = a.UpdatedAt; + IsBosText = BoolToYesNo(IsBos); + } + + // Convert boolean to YES/NO string for display + public static string BoolToYesNo(bool value) => value ? "YES" : "NO"; + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/PlayerManagement/PlayerManagement.cs b/src/PlayerManagement/PlayerManagement.cs index df6dd5b..d53e1d4 100644 --- a/src/PlayerManagement/PlayerManagement.cs +++ b/src/PlayerManagement/PlayerManagement.cs @@ -1,6 +1,9 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.VisualBasic.ApplicationServices; using NLog; using System.Text; +using System.Windows; +using Tailgrab.Clients.VRChat; using Tailgrab.Common; using Tailgrab.LineHandler; using Tailgrab.Models; @@ -18,7 +21,11 @@ public enum EventType Print, PenActivity, AvatarChange, - Moderation + Moderation, + GroupWatch, + ProfileWatch, + AvatarWatch, + Emoji } public DateTime EventTime { get; set; } = DateTime.Now; @@ -32,6 +39,47 @@ public PlayerEvent(EventType type, string eventDescription) } } + public class PlayerInventory + { + public string InventoryId { get; set; } + public string ItemName { get; set; } + public string ItemUrl { get; set; } + public string InventoryType { get; set; } + public string AIEvaluation { get; set; } + public DateTime SpawnedAt { get; set; } + public PlayerInventory(string inventoryId, string itemName, string itemUrl, string inventoryType, string aIEvaluation) + { + InventoryId = inventoryId; + ItemName = itemName; + SpawnedAt = DateTime.Now; + ItemUrl = itemUrl; + InventoryType = inventoryType; + AIEvaluation = aIEvaluation; + } + } + + public class PlayerPrint + { + public string PrintId { get; set; } + public string OwnerId { get; set; } + public DateTime Timestamp { get; set; } + public DateTime CreatedAt { get; set; } + public string PrintUrl { get; set; } + public string AIEvaluation { get; set; } + public string AuthorName { get; set; } + + public PlayerPrint(VRChat.API.Model.Print p, string aiEvaluation) + { + PrintId = p.Id; + OwnerId = p.OwnerId; + Timestamp = DateTime.Now; + CreatedAt = p.CreatedAt; + PrintUrl = p.Files.Image; + AuthorName = p.AuthorName; + AIEvaluation = aiEvaluation; + } + } + public class Player { public string UserId { get; set; } @@ -42,17 +90,18 @@ public class Player public DateTime InstanceStartTime { get; set; } public DateTime? InstanceEndTime { get; set; } public List Events { get; set; } = new List(); + public List Inventory { get; set; } = new List(); public SessionInfo Session { get; set; } public string? LastStickerUrl { get; set; } = string.Empty; - public Dictionary PrintData = new Dictionary(); + public Dictionary PrintData = new Dictionary(); public string? UserBio { get; set; } public string? AIEval { get; set; } public bool IsWatched { get { - if (IsAvatarWatch || IsGroupWatch || IsProfileWatch) + if (IsAvatarWatch || IsGroupWatch || IsProfileWatch || IsEmojiWatch || IsPrintWatch) { return true; } @@ -77,7 +126,15 @@ public string WatchCode } if (IsProfileWatch) { - code += "P"; + code += "B"; + } + if (IsEmojiWatch) + { + code += "E"; + } + if (IsPrintWatch) + { + code += "B"; } return code; @@ -87,6 +144,8 @@ public string WatchCode public bool IsAvatarWatch { get; set; } = false; public bool IsGroupWatch { get; set; } = false; public bool IsProfileWatch { get; set; } = false; + public bool IsEmojiWatch { get; set; } = false; + public bool IsPrintWatch { get; set; } = false; public Player(string userId, string displayName, SessionInfo session) @@ -133,7 +192,7 @@ public string ToString(bool full) sb.AppendLine("Events:"); foreach (var ev in PrintData.Values) { - sb.AppendLine($" - {ev.Timestamp:u} {ev.Id} {ev.AuthorName} {ev.OwnerId}"); + sb.AppendLine($" - {ev.CreatedAt:u} {ev.PrintId} {ev.AuthorName} {ev.AIEvaluation}"); } } @@ -162,10 +221,12 @@ public class SessionInfo { public string WorldId { get; set; } public string InstanceId { get; set; } + public DateTime startDateTime { get; } = DateTime.Now; public SessionInfo(string worldId, string instanceId) { WorldId = worldId; InstanceId = instanceId; + startDateTime = DateTime.Now; } } @@ -193,11 +254,10 @@ public class PlayerManager { private ServiceRegistry serviceRegistry; - private static Dictionary playersByNetworkId = new Dictionary(); private static Dictionary playersByUserId = new Dictionary(); - private static Dictionary playersByDisplayName = new Dictionary(); + private static Dictionary userIdByNetworkId = new Dictionary(); + private static Dictionary userIdByDisplayName = new Dictionary(); private static Dictionary avatarByDisplayName = new Dictionary(); - private static List avatarsInSession = new List(); public static readonly AnsiColor COLOR_PREFIX_LEAVE = AnsiColor.Yellow; public static readonly AnsiColor COLOR_PREFIX_JOIN = AnsiColor.Green; @@ -213,6 +273,31 @@ public PlayerManager(ServiceRegistry registry) serviceRegistry = registry; } + public Player? GetPlayerByDisplayName(string displayName) + { + if (userIdByDisplayName.TryGetValue(displayName, out string? userId)) + { + return GetPlayerByUserId(userId); + } + return null; + } + + public Player? GetPlayerByNetworkId(int networkId) + { + if (userIdByNetworkId.TryGetValue(networkId, out string? userId)) + { + return GetPlayerByUserId(userId); + } + return null; + } + + public Player? GetPlayerByUserId(string userId) + { + playersByUserId.TryGetValue(userId, out Player? player); + return player; + } + + public void OnPlayerChanged(PlayerChangedEventArgs.ChangeType changeType, Player player) { try @@ -225,6 +310,22 @@ public void OnPlayerChanged(PlayerChangedEventArgs.ChangeType changeType, Player } } + public void OnPlayerChanged(PlayerChangedEventArgs.ChangeType changeType, string displayName) + { + try + { + Player? player = GetPlayerByDisplayName(displayName); + if (player != null ) + { + PlayerChanged?.Invoke(null, new PlayerChangedEventArgs(changeType, player)); + } + } + catch (Exception ex) + { + logger.Error(ex, "Error raising PlayerChanged event"); + } + } + public void UpdateCurrentSession(string worldId, string instanceId) { CurrentSession = new SessionInfo(worldId, instanceId); @@ -233,7 +334,6 @@ public void UpdateCurrentSession(string worldId, string instanceId) public void PlayerJoined(string userId, string displayName, AbstractLineHandler handler) { Player? player = null; - PlayerChangedEventArgs.ChangeType changeType = PlayerChangedEventArgs.ChangeType.Added; if (!playersByUserId.ContainsKey(userId)) { player = new Player(userId, displayName, CurrentSession); @@ -241,8 +341,6 @@ public void PlayerJoined(string userId, string displayName, AbstractLineHandler { logger.Info($"{COLOR_PREFIX_JOIN.GetAnsiEscape()}Player Joined: {displayName} (ID: {userId}){COLOR_RESET.GetAnsiEscape()}"); } - - changeType = PlayerChangedEventArgs.ChangeType.Added; } else { @@ -253,12 +351,10 @@ public void PlayerJoined(string userId, string displayName, AbstractLineHandler // remove old display-name mapping if present if (!string.IsNullOrEmpty(player.DisplayName)) { - playersByDisplayName.Remove(player.DisplayName); + userIdByDisplayName.Remove(player.DisplayName); } player.DisplayName = displayName; } - - changeType = PlayerChangedEventArgs.ChangeType.Added; } if (player == null) @@ -267,12 +363,13 @@ public void PlayerJoined(string userId, string displayName, AbstractLineHandler return; } + // Check for existing avatar mapping if (avatarByDisplayName.TryGetValue(displayName, out string? avatarName)) { if (avatarName != null) { player.AvatarName = avatarName; - AddPlayerEventByDisplayName(displayName, PlayerEvent.EventType.AvatarChange, $"Joined with Avatar: {avatarName}"); + player.Events.Add(new PlayerEvent(PlayerEvent.EventType.AvatarChange, $"Joined with Avatar: {avatarName}")); if (handler.LogOutput) { logger.Info($"{COLOR_PREFIX_JOIN.GetAnsiEscape()}\tAvatar on Join: {avatarName}{COLOR_RESET.GetAnsiEscape()}"); @@ -282,14 +379,15 @@ public void PlayerJoined(string userId, string displayName, AbstractLineHandler serviceRegistry.GetOllamaAPIClient().CheckUserProfile(userId); playersByUserId[userId] = player; - playersByDisplayName[displayName] = player; + userIdByDisplayName[displayName] = player.UserId; - OnPlayerChanged(changeType, player); + OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Added, player); } public void PlayerLeft(string displayName, AbstractLineHandler handler) { - if (playersByDisplayName.TryGetValue(displayName, out Player? player)) + Player? player = GetPlayerByDisplayName(displayName); + if (player != null) { player.InstanceEndTime = DateTime.Now; TimeSpan timeDifference = (TimeSpan)(player.InstanceEndTime - player.InstanceStartTime); @@ -320,8 +418,9 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler) // Raise event with updated player before removing from internal dictionaries OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Removed, player); - playersByDisplayName.Remove(displayName); - playersByNetworkId.Remove(player.NetworkId); + userIdByDisplayName.Remove(displayName); + avatarByDisplayName.Remove(displayName); + userIdByNetworkId.Remove(player.NetworkId); playersByUserId.Remove(player.UserId); if (handler.LogOutput) { @@ -330,31 +429,13 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler) } } - public Player? GetPlayerByNetworkId(int networkId) - { - playersByNetworkId.TryGetValue(networkId, out Player? player); - return player; - } - - public Player? GetPlayerByUserId(string userId) - { - playersByUserId.TryGetValue(userId, out Player? player); - return player; - } - - public Player? GetPlayerByDisplayName(string displayName) - { - playersByDisplayName.TryGetValue(displayName, out Player? player); - return player; - } - public Player? AssignPlayerNetworkId(string displayName, int networkId) { - if (playersByDisplayName.TryGetValue(displayName, out Player? player)) + Player? player = GetPlayerByDisplayName(displayName); + if (player != null) { player.NetworkId = networkId; - playersByNetworkId[networkId] = player; - OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); + userIdByNetworkId[networkId] = player.UserId; } return player; @@ -378,10 +459,9 @@ public void ClearAllPlayers(AbstractLineHandler handler) OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Removed, player); } - playersByNetworkId.Clear(); + userIdByNetworkId.Clear(); playersByUserId.Clear(); - playersByDisplayName.Clear(); - avatarsInSession.Clear(); + userIdByDisplayName.Clear(); // Also a global cleared notification (consumers may want to reset) OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Cleared, new Player("", "", CurrentSession) { InstanceStartTime = DateTime.MinValue }); @@ -405,24 +485,21 @@ public void LogAllPlayers(AbstractLineHandler handler) public Player? AddPlayerEventByDisplayName(string displayName, PlayerEvent.EventType eventType, string eventDescription) { - if (playersByDisplayName.TryGetValue(displayName, out Player? player)) + + if(userIdByDisplayName.TryGetValue(displayName, out string? userId)) { - PlayerEvent newEvent = new PlayerEvent(eventType, eventDescription); - player.AddEvent(newEvent); - OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); - return player; + return AddPlayerEventByUserId(userId, eventType, eventDescription); } return null; } - public Player? AddPlayerEventByUserId(string displayName, PlayerEvent.EventType eventType, string eventDescription) + public Player? AddPlayerEventByUserId(string userId, PlayerEvent.EventType eventType, string eventDescription) { - if (playersByUserId.TryGetValue(displayName, out Player? player)) + if (playersByUserId.TryGetValue(userId, out Player? player)) { PlayerEvent newEvent = new PlayerEvent(eventType, eventDescription); player.AddEvent(newEvent); - OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); return player; } @@ -431,25 +508,30 @@ public void LogAllPlayers(AbstractLineHandler handler) public void SetAvatarForPlayer(string displayName, string avatarName) { + avatarByDisplayName[displayName] = avatarName; + bool watchedAvatar = serviceRegistry.GetAvatarManager().CheckAvatarByName(avatarName); if (watchedAvatar) { logger.Info($"{COLOR_PREFIX_LEAVE.GetAnsiEscape()}Watched Avatar Detected for Player {displayName}: {avatarName}{COLOR_RESET.GetAnsiEscape()}"); } - avatarByDisplayName[displayName] = avatarName; - if (playersByDisplayName.TryGetValue(displayName, out var p)) + Player? player = GetPlayerByDisplayName(displayName); + if (player != null) { - p.IsAvatarWatch = watchedAvatar; - p.AvatarName = avatarName; - OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, p); - } + player.IsAvatarWatch = watchedAvatar; + player.AvatarName = avatarName; + AddPlayerEventByDisplayName(displayName ?? string.Empty, PlayerEvent.EventType.AvatarWatch, $"User switched to Avatar : {avatarName}"); - if (!avatarsInSession.Contains(avatarName)) - { - avatarsInSession.Add(avatarName); - } + if (watchedAvatar) + { + player.PenActivity = $"AV: {avatarName}"; + AddPlayerEventByDisplayName(displayName ?? string.Empty, PlayerEvent.EventType.AvatarWatch, $"User has used a watched Avatar : {avatarName}"); + + } + OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); + } } private void PrintPlayerInfo(Player player) @@ -459,24 +541,115 @@ private void PrintPlayerInfo(Player player) internal void AddPenEventByDisplayName(string displayName, string eventText) { - if (playersByDisplayName.TryGetValue(displayName, out Player? player)) + Player? player = GetPlayerByDisplayName(displayName); + if (player != null) { player.PenActivity = eventText; OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); } } - internal void AddInventorySpawn(string inventoryId) + internal async void AddInventorySpawn(string userId, string inventoryId) { + Player? player = GetPlayerByUserId(userId); + if (player != null) + { + string itemName = "Unknown Item"; + string itemUrl = ""; + string itemContent = ""; + string inventoryType = "Unknown Type"; + string aiEvaluation = "OK"; + try + { + var inventoryItem = await serviceRegistry.GetVRChatAPIClient()?.GetUserInventoryItem(userId, inventoryId)!; + if (inventoryItem != null) + { + itemName = inventoryItem.Name ?? inventoryItem.ItemType ?? "Unknown Item"; + itemUrl = inventoryItem.ImageUrl ?? ""; + itemContent = inventoryItem.Metadata?.ImageUrl ?? itemUrl; + inventoryType = inventoryItem.ItemTypeLabel ?? "Unknown Type"; + + logger.Info($"Fetched inventory item: {itemName} / ({inventoryItem.ItemTypeLabel}) for user {userId}"); + } + } + catch (Exception ex) + { + logger.Warn($"Failed to fetch inventory item {inventoryId} / {inventoryType} for user {userId}: {ex.Message}"); + } + + if (inventoryType.Contains("Emoji") || inventoryType.Contains("Sticker")) + { + var ollamaClient = serviceRegistry.GetOllamaAPIClient(); + if (ollamaClient != null) + { + string? evaluated = await ollamaClient.ClassifyImageList(userId, inventoryId, new List{ itemUrl, itemContent }); + if (evaluated != null) + { + aiEvaluation = EvaluatImage(evaluated) ?? "OK"; + logger.Info($"Ollama classification for inventory item {inventoryId}: {aiEvaluation}: {evaluated}"); + if (!aiEvaluation.Equals("OK")) + { + AddPlayerEventByUserId(userId, PlayerEvent.EventType.Emoji, $"AI Evaluation: Spawned Item {itemName} ({inventoryId}) was classified {evaluated}"); + player.PenActivity = $"EM: {aiEvaluation}"; + player.IsEmojiWatch = true; + } + } + } + + PlayerInventory inventory = new PlayerInventory(inventoryId, itemName, itemUrl, inventoryType, aiEvaluation); + player.Inventory.Add(inventory); + + AddPlayerEventByUserId(userId, PlayerEvent.EventType.Emoji, $"Spawned Item: {itemName} ({inventoryId})"); + OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); + } + } + } + + private static string? EvaluatImage(string? imageEvaluation) + { + if (string.IsNullOrEmpty(imageEvaluation)) + { + return null; + } + + if (CheckLines(imageEvaluation, "Sexual Content")) + { + return "Sexual Content"; + } + else if (CheckLines(imageEvaluation, "Racism")) + { + return "Racism"; + } + else if (CheckLines(imageEvaluation, "Gore")) + { + return "Gore"; + } + + return null; + } + private static bool CheckLines(string input, string knownString) + { + string[] lines = input.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + + if (lines.Length < 2) + { + return false; + } + + bool firstLineContains = lines[0].Contains(knownString); + + return firstLineContains; } internal void AddStickerEvent(string displayName, string userId, string fileURL) { - if (playersByDisplayName.TryGetValue(displayName, out Player? player)) + Player? player = GetPlayerByDisplayName(displayName); + if (player != null) { player.LastStickerUrl = fileURL; AddPlayerEventByDisplayName(displayName, PlayerEvent.EventType.Sticker, $"Spawned sticker: {fileURL}"); + OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); } } @@ -485,7 +658,7 @@ internal void CompactDatabase() serviceRegistry.GetAvatarManager().CompactDatabase(); } - internal void AddPrintData(string printId) + internal async void AddPrintData(string printId) { if (serviceRegistry.GetVRChatAPIClient() != null) { @@ -494,12 +667,82 @@ internal void AddPrintData(string printId) { if (playersByUserId.TryGetValue(printInfo.OwnerId, out Player? player)) { - player.PrintData[printId] = printInfo; + string? evaluated = string.Empty; + var ollamaClient = serviceRegistry.GetOllamaAPIClient(); + if (ollamaClient != null) + { + string aiEvaluation = "OK"; + List imageUrls = new List(); + imageUrls.Add(printInfo.Files.Image); + evaluated = await ollamaClient.ClassifyImageList(printInfo.OwnerId, printInfo.Id, imageUrls); + if (evaluated != null) + { + aiEvaluation = EvaluatImage(evaluated) ?? "OK"; + logger.Info($"Ollama classification for inventory item {printInfo.Id}: {aiEvaluation}: {evaluated}"); + if( !aiEvaluation.Equals("OK")) + { + AddPlayerEventByUserId(printInfo.OwnerId, PlayerEvent.EventType.Print, $"AI Evaluation: Print {printId} was classified {evaluated}"); + player.PenActivity = "PR: " + aiEvaluation; + player.IsPrintWatch = true; + } + } + } + + player.PrintData[printId] = new PlayerPrint( printInfo, evaluated ?? "Not Evaluated" ); logger.Info($"Added Print {printId} for Player {player.DisplayName} (ID: {printInfo.OwnerId})"); } AddPlayerEventByUserId(printInfo.OwnerId, PlayerEvent.EventType.Print, $"Dropped Print {printId}"); } } } + + public GroupInfo? AddUpdateGroupFromVRC(string? groupId) + { + if (string.IsNullOrEmpty(groupId)) + return null; + + try + { + VRChatClient vrcClient = serviceRegistry.GetVRChatAPIClient(); + VRChat.API.Model.Group? group = vrcClient.getGroupById(groupId); + if (group != null) + { + TailgrabDBContext dbContext = serviceRegistry.GetDBContext(); + GroupInfo? existing = dbContext.GroupInfos.Find(group.Id); + if (existing == null) + { + GroupInfo newEntity = new GroupInfo + { + GroupId = group.Id, + GroupName = group.Name ?? string.Empty, + CreatedAt = group.CreatedAt, + UpdatedAt = DateTime.UtcNow, + IsBos = false + }; + + dbContext.GroupInfos.Add(newEntity); + dbContext.SaveChanges(); + return newEntity; + } + else + { + existing.GroupId = group.Id; + existing.GroupName = group.Name ?? string.Empty; + existing.CreatedAt = group.CreatedAt; + existing.UpdatedAt = DateTime.UtcNow; + dbContext.GroupInfos.Update(existing); + dbContext.SaveChanges(); + return existing; + } + + } + } + catch (Exception ex) + { + logger.Warn($"Failed to fetch Group: {ex.Message}"); + } + + return null; + } } } diff --git a/src/PlayerManagement/TailgrabPanel.xaml b/src/PlayerManagement/TailgrabPanel.xaml new file mode 100644 index 0000000..f38b324 --- /dev/null +++ b/src/PlayerManagement/TailgrabPanel.xaml @@ -0,0 +1,1271 @@ + + + + + + + + + + + + + + + + + + + + + + + + +