diff --git a/AppApi.cs b/AppApi.cs index abb90c18..42ed92e2 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -18,7 +18,6 @@ using Windows.UI.Notifications; using Windows.Data.Xml.Dom; using librsync.net; using System.Net.Sockets; -using System.Text.Json.Serialization; namespace VRCX { @@ -232,6 +231,11 @@ namespace VRCX return CpuMonitor.Instance.CpuUsage; } + public string GetImage(string url, string fileId, string version) + { + return ImageCache.GetImage(url, fileId, version); + } + public void DesktopNotification(string BoldText, string Text, string Image) { XmlDocument toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastImageAndText02); @@ -239,8 +243,7 @@ namespace VRCX String imagePath = Path.Combine(Program.BaseDirectory, "VRCX.ico"); if (!String.IsNullOrEmpty(Image)) { - imagePath = Path.Combine(Program.AppDataDirectory, "cache\\toast"); - File.WriteAllBytes(imagePath, Convert.FromBase64String(Image)); + imagePath = Image; } stringElements[0].AppendChild(toastXml.CreateTextNode(BoldText)); stringElements[1].AppendChild(toastXml.CreateTextNode(Text)); @@ -274,12 +277,11 @@ namespace VRCX { UseBase64Icon = true; Icon = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHaGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMS0wNC0wOFQxNjozMzoxMCsxMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo1NTE2MWIyMi1hYzgxLTY3NDYtODAyYi1kODIzYWFmN2RjYjciIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo3ZjJjNTA2ZS02YTVhLWRhNGEtOTg5Mi02NDZiMzQ0MGQxZTgiPiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT5hZG9iZTpkb2NpZDpwaG90b3Nob3A6NmJmOGE5MTgtY2QzZS03OTRjLTk3NzktMzM0YjYwZWJiNTYyPC9yZGY6bGk+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6RG9jdW1lbnRBbmNlc3RvcnM+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N2YyYzUwNmUtNmE1YS1kYTRhLTk4OTItNjQ2YjM0NDBkMWU4IiBzdEV2dDp3aGVuPSIyMDIxLTA0LTA4VDE0OjU3OjAxKzEyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmJhM2ZjODI3LTM0ZjQtYjU0OC05ZGFiLTZhMTZlZmQzZjAxMSIgc3RFdnQ6d2hlbj0iMjAyMS0wNC0wOFQxNTowMTozMSsxMjowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo2YTY5MmQzYi03ZTJkLTNiNGUtYTMzZC1hN2MwOTNlOGU0OTkiIHN0RXZ0OndoZW49IjIwMjEtMDQtMDhUMTY6MzM6MTArMTI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4XAd9sAAAFM0lEQVR42u2aWUhjVxjHjVpf3Iraoh3c4ksFx7ZYahV8EHEBqdQHFdsHQRRxpcyDIDNFpdSK+iBKUcTpmy/iglVrtT4oYsEq7hP3RGXcqqY6invy9Xy3OdPEE5PY5pKb5P7hTyA5y/1+Ofc7y70OAOBgz3YQAYgARAAiABGACEAEIAIQAYgADBT6V4HErcRbxCAwy4nriN/DC+UDADb8swADv++fiN3MDeAJ8be0k9HRUbi4uACUWq22qFFvzt5AZ1enNoSvzJ4DiJ5j412dXSBUVf9QTQH08gHgF2x8b2/P0nGqNGa0ML9AAazyAeA3bPzg4MDoFV5fX8PZ2RlcXl7qGL83JjKsVeT2UpHyaqxzdXXFtUVvOVpMYx3JFfK3CZEPAL9i4/v7+0aDwDL5+fmQl5cHBQUFnHNzc6GsrAzW19cNBQ8dHR3q7OxsFamvxnrFxcWQnp4O4+PjRvtdW1ujANYtCgBVWlqqN0vn5ORw/6o+TU1Nga+vL1MnMTERtre3rQvA3d0dZGZmMsG4ublBW1sbU/7k5ATi4+OZ8uHh4bC5uWlSn4ICQC/I39+fCSo0NBRWV1d1M3h1NVPOw8MDenp6HtWfoACg8N92dnZmgisqKuISI2pkZAS8vLyYMngb3dzcWDcAvBUKCwuZ4FxdXWFwcJDLB1FRUczvcXFxcHx8/Ki+BAkAtbW1BZGRkUyQsbGx3Gzh5OSk831QUJBJWd9qAKD6+/vB29tbJ1CJRMIE7+7uDk1NTf+pD0EDwFuhoqKCC9rQZiYrKwtub29tDwBqZ2cHUlNTwdHRkQkcwURHRxtcKFk9ANTAwAB4enoyAHCmqKys/F9tCx4ATnuY9B4a/mFhYTA3N2e7AFpaWoweaKSkpHCbH5sDMDMzw01vxgC4uLhAfX29oAHo3Yoa0vn5OSQnJzPBZmRkQFpaGjMz+Pn5wdjYmGAB3D0WQG1tLRM8Bjk7OwsKhQICAwOZ3xMSEkw6e7AEANVjAAwPD3ObmvsBVlVVgUr1z8FOQ0MD8zsukMrLyx+1JhBcDtjd3YWIiAgmOLwdtP9dTHpJSUl6d4M4bVolADzdKSkpYYIKCAjgdn/3NT8/Dz4+Pkz5mJgYkw5DBAUAh3ZzczOzDcYVYE1NzYNL5bq6Or1LZVw7nJ6eWg8APMHBRQ0ehkilUggODuaSHp4QGdriHh0dcTMDlsV6ISEhXF0cGb29vRYHMGTqqTCWmZiYgKWlJVheXgaZTMatAw4PD43WVSqVMD09zdVD48kRtiWXy98mzYe0Id+gADb4ADCMjSuPlYJ9MKLYVFAAm3wAaMbGFxcXBQugu7ubAviDDwCfY+N4Ro/DVGjCmUIrcX7P1+PxfdpJ68tWGBoagr7+PrMZH3DiwglnBGPCtQOWxeSIM45W8IvEUr4AfEG8xPcj7sbGRqMAVpZX9NWdIv6Ur/cDqD4k/o64j/h34jEzeUTTHhdMX2+fQQCyVzIa9KXmwe0z4hB6kXwCQL2DLyEQ+xK/byZ7EfsRN1AICwsLDwLAKVZTDkfkZ8RO2hfINwA+9YQ+iUYf/nloDADe80/vN2LNAFCRxGsYx4vnL/QmRS0Ar4g/sjUAqC/pKGhvb7dLAKhyCmFyctIuAbxL3EEhaL+eowVARvyxrQJASYlnKAS6IbInAKg44lMKAYU7Ra1p8BNbB4D6hvgGY8MlMG6PNQBWiCPsAYAL8Y96lr+4ivzAHgDQpPiS+EwikfxFPl8Tf00s4RWA+Lq8CEAEIAIQAYgARAA26b8BaVJkoY+4rDoAAAAASUVORK5CYII="; - } + } else { UseBase64Icon = false; - Icon = Path.Combine(Program.AppDataDirectory, "cache\\toast"); - File.WriteAllBytes(Icon, Convert.FromBase64String(Image)); + Icon = Image; } IPAddress broadcastIP = IPAddress.Parse("127.0.0.1"); @@ -306,16 +308,16 @@ namespace VRCX var Location = Path.Combine(Program.AppDataDirectory, "update.exe"); WebClient client = new WebClient(); client.Headers.Add("user-agent", AppVersion); - client.DownloadFile(new System.Uri(url), Location); + client.DownloadFile(new Uri(url), Location); } public void RestartApplication() { - System.Diagnostics.Process VRCXProcess = new System.Diagnostics.Process(); + Process VRCXProcess = new Process(); VRCXProcess.StartInfo.FileName = Path.Combine(Program.BaseDirectory, "VRCX.exe"); VRCXProcess.StartInfo.UseShellExecute = false; VRCXProcess.Start(); - System.Environment.Exit(0); + Environment.Exit(0); } public bool CheckForUpdateExe() diff --git a/ImageCache.cs b/ImageCache.cs new file mode 100644 index 00000000..fed3a75e --- /dev/null +++ b/ImageCache.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Net; +using System.Linq; + +namespace VRCX +{ + class ImageCache + { + private static readonly string cacheLocation = Path.Combine(Program.AppDataDirectory, "ImageCache"); + + public static string GetImage(string url, string fileId, string version) + { + var directoryLocation = Path.Combine(cacheLocation, fileId); + var fileLocation = Path.Combine(directoryLocation, $"{version}.png"); + + if (File.Exists(fileLocation)) + { + Directory.SetLastWriteTime(directoryLocation, DateTime.Now); + return fileLocation; + } + + if (Directory.Exists(directoryLocation)) + Directory.Delete(directoryLocation, true); + Directory.CreateDirectory(directoryLocation); + + using (var client = new WebClient()) + { + client.Headers.Add("user-agent", "VRCX"); + client.DownloadFile(url, fileLocation); + } + + int cacheSize = Directory.GetDirectories(cacheLocation).Length; + if (cacheSize > 1100) + CleanImageCache(); + + return fileLocation; + } + + private static void CleanImageCache() + { + DirectoryInfo dirInfo = new DirectoryInfo(cacheLocation); + var folders = dirInfo.GetDirectories().OrderBy(p => p.LastWriteTime); + int i = 0; + foreach (DirectoryInfo folder in folders.Reverse()) + { + i++; + if (i > 1000) + folder.Delete(true); + } + } + } +} \ No newline at end of file diff --git a/LogWatcher.cs b/LogWatcher.cs index 4ef6387b..d916c472 100644 --- a/LogWatcher.cs +++ b/LogWatcher.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; +using System.Text.Json.Serialization; using System.Threading; namespace VRCX @@ -22,6 +23,11 @@ namespace VRCX public string RecentWorldName; public string LastVideoURL; public bool ShaderKeywordsLimitReached = false; + public bool incomingJson; + public string jsonChunk; + public string jsonDate; + public string onJoinPhotonDisplayName; + public string photonEvent; } public static readonly LogWatcher Instance; @@ -33,6 +39,7 @@ namespace VRCX private bool m_ResetLog; private bool m_FirstRun = true; private static DateTime tillDate = DateTime.Now; + private static IDictionary photonEvent7 = new Dictionary(); // NOTE // FileSystemWatcher() is unreliable @@ -183,6 +190,21 @@ namespace VRCX break; } + if (logContext.incomingJson) + { + logContext.jsonChunk += line; + if (line == "}}") + { + var data = logContext.jsonChunk.Replace("{{", "{").Replace("}}", "}"); + ParseLogPhotonEvent(fileInfo, data, logContext.jsonDate, logContext.photonEvent); + logContext.incomingJson = false; + logContext.jsonChunk = String.Empty; + logContext.jsonDate = String.Empty; + logContext.photonEvent = String.Empty; + } + continue; + } + // 2020.10.31 23:36:28 Log - [VRCFlowManagerVRC] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd // 2021.02.03 10:18:58 Log - [DŽDŽDžDžDžDŽDŽDžDžDŽDžDžDžDžDŽDŽDŽDžDžDŽDŽDžDžDžDžDŽDžDžDžDžDŽDŽDŽDŽDŽDžDŽDžDŽDŽDŽDžDžDŽDžDžDž] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd @@ -209,7 +231,21 @@ namespace VRCX var offset = 34; if (line[offset] == '[') { - if (ParseLogOnPlayerJoinedOrLeft(fileInfo, logContext, line, offset) == true || + if (string.Compare(line, offset, "[Network Data] OnEvent: PLAYER: ", 0, 33, StringComparison.Ordinal) == 0) + { + logContext.photonEvent = line.Substring(offset + 33); + logContext.incomingJson = true; + logContext.jsonChunk = String.Empty; + logContext.jsonDate = ConvertLogTimeToISO8601(line); + } + else if (string.Compare(line, offset, "[Network Data] OnEvent: SYSTEM ", 0, 31, StringComparison.Ordinal) == 0) + { + logContext.photonEvent = line.Substring(offset + 31); + logContext.incomingJson = true; + logContext.jsonChunk = String.Empty; + logContext.jsonDate = ConvertLogTimeToISO8601(line); + } + else if (ParseLogOnPlayerJoinedOrLeft(fileInfo, logContext, line, offset) == true || ParseLogLocation(fileInfo, logContext, line, offset) == true || ParseLogLocationDestination(fileInfo, logContext, line, offset) == true || ParseLogPortalSpawn(fileInfo, logContext, line, offset) == true || @@ -218,8 +254,9 @@ namespace VRCX ParseLogJoinBlocked(fileInfo, logContext, line, offset) == true || ParseLogAvatarPedestalChange(fileInfo, logContext, line, offset) == true || ParseLogVideoError(fileInfo, logContext, line, offset) == true || - ParseLogVideoPlay(fileInfo, logContext, line, offset) == true || - ParseLogWorldVRCX(fileInfo, logContext, line, offset) == true) + ParseLogVideoChange(fileInfo, logContext, line, offset) == true || + ParseLogWorldVRCX(fileInfo, logContext, line, offset) == true || + ParseLogPhotonId(fileInfo, logContext, line, offset) == true) { continue; } @@ -289,16 +326,26 @@ namespace VRCX // 2021.02.03 10:18:58 Log - [DŽDŽDžDžDžDŽDŽDžDžDŽDžDžDžDžDŽDŽDŽDžDžDŽDŽDžDžDžDžDŽDžDžDžDžDŽDŽDŽDŽDŽDžDŽDžDŽDŽDŽDžDžDŽDžDžDž] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd // 2021.06.23 12:02:56 Log - [Behaviour] Entering Room: VRChat Home - if (string.Compare(line, offset, "[Behaviour] Entering Room: ", 0, 27, StringComparison.Ordinal) == 0) + if (line.Contains("] Entering Room: ")) { - var worldName = line.Substring(offset + 27); + var lineOffset = line.LastIndexOf("] Entering Room: "); + if (lineOffset < 0) + return false; + lineOffset += 17; + + var worldName = line.Substring(lineOffset); logContext.RecentWorldName = worldName; return true; } - if (string.Compare(line, offset, "[Behaviour] Joining wrld_", 0, 25, StringComparison.Ordinal) == 0) + if (line.Contains("] Joining wrld_")) { - var location = line.Substring(offset + 20); + var lineOffset = line.LastIndexOf("] Joining "); + if (lineOffset < 0) + return false; + lineOffset += 10; + + var location = line.Substring(lineOffset); AppendLog(new[] { @@ -309,6 +356,9 @@ namespace VRCX logContext.RecentWorldName }); + photonEvent7 = new Dictionary(); + logContext.onJoinPhotonDisplayName = String.Empty; + return true; } @@ -320,9 +370,14 @@ namespace VRCX // 2021.09.02 00:02:12 Log - [Behaviour] Destination set: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd:15609~private(usr_032383a7-748c-4fb2-94e4-bcb928e5de6b)~nonce(72CC87D420C1D49AEFFBEE8824C84B2DF0E38678E840661E) // 2021.09.02 00:49:15 Log - [Behaviour] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd - if (string.Compare(line, offset, "[Behaviour] Destination fetching: ", 0, 34, StringComparison.Ordinal) == 0) + if (line.Contains("] Destination fetching: ")) { - var location = line.Substring(offset + 34); + var lineOffset = line.LastIndexOf("] Destination fetching: "); + if (lineOffset < 0) + return false; + lineOffset += 24; + + var location = line.Substring(lineOffset); AppendLog(new[] { @@ -335,20 +390,6 @@ namespace VRCX return true; } - // 2021.10.04 11:54:16 Log - [Behaviour] OnLeftRoom - - if (string.Compare(line, offset, "[Behaviour] OnLeftRoom", 0, 22, StringComparison.Ordinal) == 0) - { - AppendLog(new[] - { - fileInfo.Name, - ConvertLogTimeToISO8601(line), - "location-destination" - }); - - return true; - } - return false; } @@ -365,16 +406,19 @@ namespace VRCX // 2021.06.23 11:41:16 Log - [Behaviour] Initialized PlayerAPI "Natsumi-sama" is local - if (string.Compare(line, offset, "[Behaviour] Initialized PlayerAPI \"", 0, 35, StringComparison.Ordinal) == 0) + if (line.Contains("] Initialized PlayerAPI \"")) { + var lineOffset = line.LastIndexOf("] Initialized PlayerAPI \""); + if (lineOffset < 0) + return false; + lineOffset += 25; + var pos = line.LastIndexOf("\" is "); if (pos < 0) - { return false; - } - var userDisplayName = line.Substring(offset + 35, pos - (offset + 35)); var userType = line.Substring(pos + 5); + var userDisplayName = line.Substring(lineOffset, pos - lineOffset); AppendLog(new[] { @@ -388,25 +432,14 @@ namespace VRCX return true; } - // fallback method - /*if (string.Compare(line, offset, "OnPlayerJoined ", 0, 15, StringComparison.Ordinal) == 0) + if (line.Contains("] OnPlayerLeft ")) { - var userDisplayName = line.Substring(offset + 15); + var lineOffset = line.LastIndexOf("] OnPlayerLeft "); + if (lineOffset < 0) + return false; + lineOffset += 15; - AppendLog(new[] - { - fileInfo.Name, - ConvertLogTimeToISO8601(line), - "player-joined", - userDisplayName - }); - - return true; - }*/ - - if (string.Compare(line, offset, "[Behaviour] OnPlayerLeft ", 0, 25, StringComparison.Ordinal) == 0) - { - var userDisplayName = line.Substring(offset + 25); + var userDisplayName = line.Substring(lineOffset); AppendLog(new[] { @@ -427,22 +460,16 @@ namespace VRCX // 2021.04.06 11:25:45 Log - [Network Processing] RPC invoked ConfigurePortal on (Clone [1600004] Portals/PortalInternalDynamic) for Natsumi-sama // 2021.07.19 04:24:28 Log - [Behaviour] Will execute SendRPC/AlwaysBufferOne on (Clone [100004] Portals/PortalInternalDynamic) (UnityEngine.GameObject) for Natsumi-sama: S: "ConfigurePortal" I: 7 F: 0 B: 255 (local master owner) - if (string.Compare(line, offset, "[Behaviour] Will execute SendRPC/AlwaysBufferOne on (Clone [", 0, 60, StringComparison.Ordinal) != 0) - { + if (!line.Contains("] Will execute SendRPC/AlwaysBufferOne on (Clone [")) return false; - } var pos = line.LastIndexOf("] Portals/PortalInternalDynamic) (UnityEngine.GameObject) for "); if (pos < 0) - { return false; - } var endPos = line.LastIndexOf(": S: \"ConfigurePortal\""); if (endPos < 0) - { return false; - } var data = line.Substring(pos + 62, endPos - (pos + 62)); @@ -462,36 +489,32 @@ namespace VRCX // 2021.04.04 12:21:06 Error - Maximum number (256) of shader keywords exceeded, keyword _TOGGLESIMPLEBLUR_ON will be ignored. // 2021.08.20 04:20:69 Error - Maximum number (384) of shader global keywords exceeded, keyword _FOG_EXP2 will be ignored. - if (logContext.ShaderKeywordsLimitReached == true) + if (line.Contains("Maximum number (384) of shader global keywords exceeded")) { - return false; + if (logContext.ShaderKeywordsLimitReached == true) + return true; + + AppendLog(new[] + { + fileInfo.Name, + ConvertLogTimeToISO8601(line), + "event", + "Shader Keyword Limit has been reached" + }); + logContext.ShaderKeywordsLimitReached = true; + + return true; } - if (string.Compare(line, offset, "Maximum number (384) of shader global keywords exceeded", 0, 55, StringComparison.Ordinal) != 0) - { - return false; - } - - AppendLog(new[] - { - fileInfo.Name, - ConvertLogTimeToISO8601(line), - "event", - "Shader Keyword Limit has been reached" - }); - logContext.ShaderKeywordsLimitReached = true; - - return true; + return false; } private bool ParseLogJoinBlocked(FileInfo fileInfo, LogContext logContext, string line, int offset) { // 2021.04.07 09:34:37 Error - [Behaviour] Master is not sending any events! Moving to a new instance. - if (string.Compare(line, offset, "[Behaviour] Master is not sending any events! Moving to a new instance.", 0, 71, StringComparison.Ordinal) != 0) - { + if (!line.Contains("] Master is not sending any events! Moving to a new instance.")) return false; - } AppendLog(new[] { @@ -500,6 +523,7 @@ namespace VRCX "event", "Joining instance blocked by master" }); + logContext.ShaderKeywordsLimitReached = true; return true; } @@ -509,9 +533,7 @@ namespace VRCX // 2021.05.07 10:48:19 Log - [Network Processing] RPC invoked SwitchAvatar on AvatarPedestal for User if (string.Compare(line, offset, "[Network Processing] RPC invoked SwitchAvatar on AvatarPedestal for ", 0, 68, StringComparison.Ordinal) != 0) - { return false; - } var data = line.Substring(offset + 68); @@ -532,9 +554,7 @@ namespace VRCX // 2021.04.08 06:40:07 Error - [Video Playback] ERROR: Private video if (string.Compare(line, offset, "[Video Playback] ERROR: ", 0, 24, StringComparison.Ordinal) != 0) - { return false; - } var data = line.Substring(offset + 24); @@ -549,29 +569,20 @@ namespace VRCX return true; } - private bool ParseLogVideoPlay(FileInfo fileInfo, LogContext logContext, string line, int offset) + private bool ParseLogVideoChange(FileInfo fileInfo, LogContext logContext, string line, int offset) { // 2021.04.20 13:37:69 Log - [Video Playback] Attempting to resolve URL 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' if (string.Compare(line, offset, "[Video Playback] Attempting to resolve URL '", 0, 44, StringComparison.Ordinal) != 0) - { return false; - } var pos = line.LastIndexOf("'"); if (pos < 0) - { return false; - } + var data = line.Substring(offset + 44); data = data.Remove(data.Length - 1); - if (logContext.LastVideoURL == data) - { - return false; - } - logContext.LastVideoURL = data; - AppendLog(new[] { fileInfo.Name, @@ -610,23 +621,18 @@ namespace VRCX // 2021.04.23 13:12:25 Log - User Natsumi-sama added URL https://www.youtube.com/watch?v=dQw4w9WgXcQ if (string.Compare(line, offset, "User ", 0, 5, StringComparison.Ordinal) != 0) - { return false; - } var pos = line.LastIndexOf(" added URL "); if (pos < 0) - { return false; - } - var displayName = line.Substring(offset + 5, pos - (offset + 5)); + var playerPlayer = line.Substring(offset + 5, pos - (offset + 5)); var data = line.Substring(pos + 11); if (logContext.LastVideoURL == data) - { return false; - } + logContext.LastVideoURL = data; AppendLog(new[] @@ -635,7 +641,7 @@ namespace VRCX ConvertLogTimeToISO8601(line), "video-play", data, - displayName + playerPlayer }); return true; @@ -646,15 +652,11 @@ namespace VRCX // 2021.01.03 05:48:58 Log - [API] Received Notification: < Notification from username:pypy, sender user id:usr_4f76a584-9d4b-46f6-8209-8305eb683661 to of type: friendRequest, id: not_3a8f66eb-613c-4351-bee3-9980e6b5652c, created at: 01/14/2021 15:38:40 UTC, details: {{}}, type:friendRequest, m seen:False, message: ""> received at 01/02/2021 16:48:58 UTC if (string.Compare(line, offset, "[API] Received Notification: <", 0, 30, StringComparison.Ordinal) != 0) - { return false; - } var pos = line.LastIndexOf("> received at "); if (pos < 0) - { return false; - } var data = line.Substring(offset + 30, pos - (offset + 30)); @@ -675,15 +677,11 @@ namespace VRCX // 2021.10.03 09:48:43 Log - [API] [101] Sending Get request to https://api.vrchat.cloud/api/1/users/usr_032383a7-748c-4fb2-94e4-bcb928e5de6b?apiKey=JlE5Jldo5Jibnk5O5hTx6XVqsJu4WJ26&organization=vrchat if (string.Compare(line, offset, "[API] [", 0, 7, StringComparison.Ordinal) != 0) - { return false; - } var pos = line.LastIndexOf("] Sending Get request to "); if (pos < 0) - { return false; - } var data = line.Substring(pos + 25); @@ -698,6 +696,174 @@ namespace VRCX return true; } + private bool ParseLogPhotonId(FileInfo fileInfo, LogContext logContext, string line, int offset) + { + // 2021.11.02 02:21:41 Log - [Behaviour] Configuring remote player VRCPlayer[Remote] 22349737 1194 + // 2021.11.02 02:21:41 Log - [Behaviour] Initialized player Natsumi-sama + + // 2021.11.10 08:06:12 Log - [Behaviour] Natsumi-sama: Limb IK + // 2021.11.10 08:06:12 Log - [Behaviour] NatsumiDa: 3 Point IK + // 2021.11.10 08:10:28 Log - [Behaviour] Initialize Limb Avatar (UnityEngine.Animator) VRCPlayer[Remote] 78614426 59 (DŽDŽDŽDžDŽDžDžDŽDžDŽDŽDžDžDŽDžDŽDžDžDžDŽDŽDŽDžDŽDŽDžDžDŽDžDžDŽDžDžDŽDžDžDžDžDŽDžDŽDžDŽDŽDŽDŽDž) False Loading + // 2021.11.10 08:57:32 Log - [Behaviour] Initialize Limb Avatar (UnityEngine.Animator) VRCPlayer[Local] 59136629 1 (DŽDŽDŽDžDŽDžDžDŽDžDŽDŽDžDžDŽDžDŽDžDžDžDŽDŽDŽDžDŽDŽDžDžDŽDžDžDŽDžDžDŽDžDžDžDžDŽDžDŽDžDŽDŽDŽDŽDž) True Loading + + if (line.Contains("] Initialize ") && line.Contains(" Avatar (UnityEngine.Animator) VRCPlayer[")) + { + var pos = -1; + + if (line.Contains(" Avatar (UnityEngine.Animator) VRCPlayer[Remote] ")) + { + pos = line.LastIndexOf(" Avatar (UnityEngine.Animator) VRCPlayer[Remote] "); + pos += 49; + } + + if (line.Contains(" Avatar (UnityEngine.Animator) VRCPlayer[Local] ")) + { + pos = line.LastIndexOf(" Avatar (UnityEngine.Animator) VRCPlayer[Local] "); + pos += 48; + } + + if (pos < 0) + return false; + + if (!String.IsNullOrEmpty(logContext.onJoinPhotonDisplayName)) + { + var endPos = line.LastIndexOf(" ("); + var photonId = line.Substring(pos + 9, endPos - (pos + 9)); + + AppendLog(new[] + { + fileInfo.Name, + ConvertLogTimeToISO8601(line), + "photon-id", + logContext.onJoinPhotonDisplayName, + photonId + }); + logContext.onJoinPhotonDisplayName = String.Empty; + + return true; + } + } + + if (line.Contains(": 3 Point IK") || line.Contains(": Limb IK")) + { + var lineOffset = line.IndexOf("] "); + if (lineOffset < 0) + return false; + lineOffset += 2; + + if (line.Contains(": 3 Point IK")) + { + var endPos = line.LastIndexOf(": 3 Point IK"); + logContext.onJoinPhotonDisplayName = line.Substring(lineOffset, endPos - lineOffset); + return true; + } + + if (line.Contains(": Limb IK")) + { + var endPos = line.LastIndexOf(": Limb IK"); + logContext.onJoinPhotonDisplayName = line.Substring(lineOffset, endPos - lineOffset); + return true; + } + } + + return false; + } + + public class VrcEvent + { + public int Code { get; set; } + public Parameters Parameters { get; set; } + public int SenderKey { get; set; } + public int CustomDataKey { get; set; } + public int Type { get; set; } + public string EventType { get; set; } + public Object Data { get; set; } + } + + public class Parameters + { + [JsonPropertyName("245")] + public _245 _245 { get; set; } + [JsonPropertyName("254")] + public int _254 { get; set; } + } + + public class _245 + { + [JsonPropertyName("$type")] + public string Type { get; set; } + [JsonPropertyName("$value")] + public string Value { get; set; } + } + + private void ParseLogPhotonEvent(FileInfo fileInfo, string data, string date, string photonEvent) + { + // 2021.09.30 04:27:11 Log - [Network Data] OnEvent: PLAYER: 253 + // 2021.09.30 04:27:40 Log - [Network Data] OnEvent: SYSTEM 255 + + if (photonEvent == "1" || photonEvent == "8" || photonEvent == "9" || photonEvent == "210") + { + return; + } + + if (photonEvent == "7") + { + var json = System.Text.Json.JsonSerializer.Deserialize(data); + var photonId = json.Parameters._254; + if (photonEvent7.ContainsKey(photonId)) + { + photonEvent7[photonId] = date; + } else + { + photonEvent7.Add(photonId, date); + } + return; + } + + if (photonEvent == "254") + { + var json = System.Text.Json.JsonSerializer.Deserialize(data); + photonEvent7.Remove(json.Parameters._254); + } + + if (photonEvent == "6") + { + var json = System.Text.Json.JsonSerializer.Deserialize(data); + byte[] bytes = Convert.FromBase64String(json.Parameters._245.Value); + try + { + var deserialization = new VRCEventDeserialization(); + var eventData = deserialization.DeserializeData(bytes); + json.Data = eventData.Data; + json.Type = eventData.Type; + json.EventType = eventData.EventType; + data = System.Text.Json.JsonSerializer.Serialize(json); + } + catch(Exception ex) + { + data = ex.ToString(); + } + } + + AppendLog(new[] + { + fileInfo.Name, + date, + "photon-event", + data + }); + } + + public IDictionary GetEvent7() + { + return photonEvent7; + } + + public void ClearEvent7() + { + photonEvent7 = new Dictionary(); + } + public string[][] Get() { Update(); diff --git a/VRCEventDeserialization.cs b/VRCEventDeserialization.cs new file mode 100644 index 00000000..f4873b8f --- /dev/null +++ b/VRCEventDeserialization.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace VRCX +{ + class VRCEventDeserialization + { + private byte[] byteData; + private int byteOffset; + private static readonly Dictionary DataType = new Dictionary { + {2, typeof(byte)}, + {3, typeof(double)}, + {4, typeof(float)}, + {5, typeof(int)}, + {6, typeof(short)}, + {7, typeof(long)}, + {8, typeof(bool)}, + {9, typeof(string)}, + {10, typeof(object[])}, + {11, typeof(IList)}, + {100, typeof(Vector2)}, + {101, typeof(Vector3)}, + {102, typeof(Vector4)}, + {103, typeof(Quaternion)} + }; + + public class EventEntry + { + public int Type; + public string EventType; + public object Data; + } + + private byte DeserializeByte() + { + return byteData[byteOffset++]; + } + + private int DeserializeInt() + { + int output = BitConverter.ToInt32(byteData, byteOffset); + byteOffset += 4; + return output; + } + + private short DeserializeShort() + { + short output = BitConverter.ToInt16(byteData, byteOffset); + byteOffset += 2; + return output; + } + + private string DeserializeString() + { + short stringLength = DeserializeShort(); + string output = Encoding.UTF8.GetString(byteData, byteOffset, stringLength); + byteOffset += stringLength; + return output; + } + + private bool DeserializeBool() + { + bool output = BitConverter.ToBoolean(byteData, byteOffset); + byteOffset++; + return output; + } + + private float DeserializeFloat() + { + float output = BitConverter.ToSingle(byteData, byteOffset); + byteOffset += 4; + return output; + } + + private double DeserializeDouble() + { + double output = BitConverter.ToDouble(byteData, byteOffset); + byteOffset += 8; + return output; + } + + private object DeserializeTypeArray() + { + short length = DeserializeShort(); + byte typeCode = DeserializeByte(); + var output = Array.CreateInstance(DataType[typeCode], length); + for (var i = 0; i < length; i++) + { + output.SetValue(DeserializeBytes(typeCode), i); + } + return output; + } + + private object[] DeserializeObjectArray() + { + short length = DeserializeShort(); + object[] output = new object[length]; + for (var i = 0; i < output.Length; i++) + { + output[i] = DeserializeBytes(); + } + return output; + } + + private long DeserializeInt64() + { + long output = BitConverter.ToInt64(byteData, byteOffset); + byteOffset += 8; + return output; + } + + private Vector2 DeserializeVector2() + { + return new Vector2(DeserializeFloat(), DeserializeFloat()); + } + + private Vector3 DeserializeVector3() + { + return new Vector3(DeserializeFloat(), DeserializeFloat(), DeserializeFloat()); + } + + private Vector4 DeserializeVector4() + { + return new Vector4(DeserializeFloat(), DeserializeFloat(), DeserializeFloat(), DeserializeFloat()); + } + + private Quaternion DeserializeQuaternion() + { + return new Quaternion(DeserializeFloat(), DeserializeFloat(), DeserializeFloat(), DeserializeFloat()); + } + + private object DeserializeBytes(byte type = 0) + { + if (type == 0) + { + type = DeserializeByte(); + } + switch (type) + { + case 1: + return null; + case 2: + return DeserializeByte(); + case 3: + return DeserializeDouble(); + case 4: + return DeserializeFloat(); + case 5: + return DeserializeInt(); + case 6: + return DeserializeShort(); + case 7: + return DeserializeInt64(); + case 8: + return DeserializeBool(); + case 9: + return DeserializeString(); + case 10: + return DeserializeObjectArray(); + case 11: + return DeserializeTypeArray(); + case 100: + return DeserializeVector2(); + case 101: + return DeserializeVector3(); + case 102: + return DeserializeVector4(); + case 103: + return DeserializeQuaternion(); + default: + throw new Exception("Ignoring data type: " + type); + } + } + + public EventEntry DeserializeData(byte[] bytes) + { + EventEntry eventEntry = new EventEntry(); + byteOffset = 0; + byteData = bytes; + byte type = DeserializeByte(); + if (type == 106) + { + byteOffset += 8; + int length = DeserializeShort(); + byteOffset += length; + eventEntry.Type = DeserializeByte(); + byteOffset += 10; + eventEntry.EventType = DeserializeString(); + byteOffset += 5; + eventEntry.Data = null; + if (byteData.Length > byteOffset + 3) + { + byteData = (byte[])DeserializeTypeArray(); + byteOffset = 0; + eventEntry.Data = DeserializeBytes(); + } + } + else + { + throw new Exception("Unexpected type: " + type); + } + return eventEntry; + } + } +} diff --git a/VRCX.csproj b/VRCX.csproj index 34a52f58..be58bb6d 100644 --- a/VRCX.csproj +++ b/VRCX.csproj @@ -1,4 +1,4 @@ - + @@ -81,6 +81,8 @@ + + diff --git a/html/src/app.js b/html/src/app.js index 1bad2f05..a2ce229c 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -1405,6 +1405,15 @@ speechSynthesis.getVoices(); // ...json }; + if ( + !json.isFriend && + $app.lastLocation.playerList.has(json.displayName) + ) { + var player = $app.lastLocation.playerList.get(json.displayName); + ref.location = 'offline'; + ref.$location_at = player.joinTime; + ref.$online_for = player.joinTime; + } ref.$location = this.parseLocation(ref.location); ref.$isVRCPlus = ref.tags.includes('system_supporter'); this.applyUserTrustLevel(ref); @@ -1458,6 +1467,7 @@ speechSynthesis.getVoices(); } } } + this.$emit('USER:APPLY', ref); return ref; }; @@ -4173,6 +4183,10 @@ speechSynthesis.getVoices(); wrist: [], lastEntryDate: '' }, + moderationAgainstTable: { + wrist: [], + lastEntryDate: '' + }, pendingUpdate: false }; @@ -4184,6 +4198,7 @@ speechSynthesis.getVoices(); this.updateSharedFeedFeedTable(forceUpdate); this.updateSharedFeedNotificationTable(forceUpdate); this.updateSharedFeedFriendLogTable(forceUpdate); + this.updateSharedFeedModerationAgainstTable(forceUpdate); var feeds = this.sharedFeed; if (!feeds.pendingUpdate) { return; @@ -4193,7 +4208,8 @@ speechSynthesis.getVoices(); feeds.gameLog.wrist, feeds.feedTable.wrist, feeds.notificationTable.wrist, - feeds.friendLogTable.wrist + feeds.friendLogTable.wrist, + feeds.moderationAgainstTable.wrist ); // OnPlayerJoining var L = API.parseLocation(this.lastLocation.location); // WebSocket dosen't update friend only instances @@ -4716,6 +4732,66 @@ speechSynthesis.getVoices(); } }; + $app.methods.updateSharedFeedModerationAgainstTable = function ( + forceUpdate + ) { + // Unblocked, Blocked, Muted, Unmuted + var data = this.moderationAgainstTable; + var i = data.length; + if (i > 0) { + if ( + data[i - 1].created_at === + this.sharedFeed.moderationAgainstTable.lastEntryDate && + forceUpdate === false + ) { + return; + } + this.sharedFeed.moderationAgainstTable.lastEntryDate = + data[i - 1].created_at; + } else { + return; + } + var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours + var wristArr = []; + var w = 0; + var wristFilter = this.sharedFeedFilters.wrist; + for (var i = data.length - 1; i > -1; i--) { + var ctx = data[i]; + if (ctx.created_at < bias) { + break; + } + var isFriend = this.friends.has(ctx.userId); + var isFavorite = API.cachedFavoritesByObjectId.has(ctx.userId); + if ( + w < 20 && + wristFilter[ctx.type] && + wristFilter[ctx.type] === 'On' + ) { + wristArr.push({ + ...ctx, + isFriend, + isFavorite + }); + ++w; + } + } + this.sharedFeed.moderationAgainstTable.wrist = wristArr; + this.sharedFeed.pendingUpdate = true; + }; + + $app.methods.queueModerationNoty = function (noty) { + noty.isFriend = false; + noty.isFavorite = false; + if (noty.userId) { + noty.isFriend = this.friends.has(noty.userId); + noty.isFavorite = API.cachedFavoritesByObjectId.has(noty.userId); + } + var notyFilter = this.sharedFeedFilters.noty; + if (notyFilter[noty.type] && notyFilter[noty.type] === 'On') { + this.playNoty(noty); + } + }; + $app.data.notyMap = []; $app.methods.playNoty = function (noty) { @@ -4871,31 +4947,27 @@ speechSynthesis.getVoices(); $app.methods.notySaveImage = async function (noty) { var imageUrl = await this.notyGetImage(noty); - var base64Image = ''; - if (imageUrl) { - try { - base64Image = await fetch(imageUrl, { - method: 'GET', - redirect: 'follow' - }) - .then((response) => response.arrayBuffer()) - .then((buffer) => { - var binary = ''; - var bytes = new Uint8Array(buffer); - var length = bytes.byteLength; - for (var i = 0; i < length; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); - }); - } catch (err) { - console.error(err); - } + var fileId = extractFileId(imageUrl); + var fileVersion = extractFileVersion(imageUrl); + if (fileId && fileVersion) { + return AppApi.GetImage(imageUrl, fileId, fileVersion); + } else if (imageUrl) { + fileVersion = imageUrl.split('/').pop(); // 1416226261.thumbnail-500.png + fileId = fileVersion.split('.').shift(); // 1416226261 + return AppApi.GetImage(imageUrl, fileId, fileVersion); } - return base64Image; + return ''; }; - $app.methods.displayOverlayNotification = function (noty, message, image) { + $app.methods.displayOverlayNotification = function ( + noty, + message, + imageFile + ) { + var image = ''; + if (imageFile) { + image = `file:///${imageFile}`; + } AppApi.ExecuteVrOverlayFunction( 'playNoty', JSON.stringify({noty, message, image}) @@ -5020,6 +5092,18 @@ speechSynthesis.getVoices(); case 'MutedOnPlayerLeft': this.speak(`Muted user ${noty.displayName} has left`); break; + case 'Blocked': + this.speak(`${noty.displayName} has blocked you`); + break; + case 'Unblocked': + this.speak(`${noty.displayName} has unblocked you`); + break; + case 'Muted': + this.speak(`${noty.displayName} has muted you`); + break; + case 'Unmuted': + this.speak(`${noty.displayName} has unmuted you`); + break; } }; @@ -5235,6 +5319,38 @@ speechSynthesis.getVoices(); image ); break; + case 'Blocked': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has blocked you`, + timeout, + image + ); + break; + case 'Unblocked': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has unblocked you`, + timeout, + image + ); + break; + case 'Muted': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has muted you`, + timeout, + image + ); + break; + case 'Unmuted': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has unmuted you`, + timeout, + image + ); + break; } }; @@ -5416,6 +5532,34 @@ speechSynthesis.getVoices(); image ); break; + case 'Blocked': + AppApi.DesktopNotification( + noty.displayName, + 'has blocked you', + image + ); + break; + case 'Unblocked': + AppApi.DesktopNotification( + noty.displayName, + 'has unblocked you', + image + ); + break; + case 'Muted': + AppApi.DesktopNotification( + noty.displayName, + 'has muted you', + image + ); + break; + case 'Unmuted': + AppApi.DesktopNotification( + noty.displayName, + 'has unmuted you', + image + ); + break; } }; @@ -7140,6 +7284,7 @@ speechSynthesis.getVoices(); break; } } + this.updateCurrentInstanceWorld(this.lastLocation.location); if (length > 0) { for (var i = length + 1; i < data.length; i++) { var ctx = data[i]; @@ -7171,6 +7316,7 @@ speechSynthesis.getVoices(); } } this.updateVRLastLocation(); + this.getCurrentInstanceUserList(); } }; @@ -7401,6 +7547,17 @@ speechSynthesis.getVoices(); }; $app.methods.lastLocationReset = function () { + this.photonLobby = new Map(); + this.photonLobbyCurrent = new Map(); + this.photonLobbyWatcherLoopStop(); + this.photonLobbyAvatars = new Map(); + this.photonLobbyJointime = new Map(); + this.moderationEventQueue = new Map(); + this.lastPortalId = ''; + this.lastPortalList = new Map(); + this.portalQueue = ''; + this.photonEventTable.data = []; + this.updateCurrentInstanceWorld(); var playerList = Array.from(this.lastLocation.playerList.values()); for (var ref of playerList) { var time = new Date().getTime() - ref.joinTime; @@ -7431,6 +7588,7 @@ speechSynthesis.getVoices(); friendList: new Map() }; this.updateVRLastLocation(); + this.getCurrentInstanceUserList(); }; $app.data.lastLocation$ = { @@ -7689,6 +7847,7 @@ speechSynthesis.getVoices(); }; this.updateVRLastLocation(); this.cancelVRChatCacheDownload(gameLog.location); + this.updateCurrentInstanceWorld(gameLog.location); } var L = API.parseLocation(gameLog.location); var entry = { @@ -7711,13 +7870,22 @@ speechSynthesis.getVoices(); gameLog.userDisplayName, userMap ); - if (this.friends.has(userId)) { - this.lastLocation.friendList.set( - gameLog.userDisplayName, - userMap - ); + if (userId) { + if (this.friends.has(userId)) { + this.lastLocation.friendList.set( + gameLog.userDisplayName, + userMap + ); + } else { + var ref = API.cachedUsers.get(userId); + if (typeof ref !== 'undefined') { + var joinTime = Date.parse(gameLog.dt); + ref.$location_at = joinTime; + } + } } this.updateVRLastLocation(); + this.getCurrentInstanceUserList(); var entry = { created_at: gameLog.dt, type: 'OnPlayerJoined', @@ -7742,7 +7910,9 @@ speechSynthesis.getVoices(); gameLog.userDisplayName ); } + this.photonLobbyAvatars.delete(userId); this.updateVRLastLocation(); + this.getCurrentInstanceUserList(); var entry = { created_at: gameLog.dt, type: 'OnPlayerLeft', @@ -7754,6 +7924,22 @@ speechSynthesis.getVoices(); database.addGamelogJoinLeaveToDatabase(entry); break; case 'portal-spawn': + if (this.portalQueue === 'skip') { + this.portalQueue = ''; + return; + } else if (this.portalQueue && gameLog.userDisplayName) { + var ref = { + id: userId, + displayName: gameLog.userDisplayName + }; + this.parsePhotonPortalSpawn( + gameLog.dt, + this.portalQueue, + ref + ); + this.portalQueue = ''; + return; + } var entry = { created_at: gameLog.dt, type: 'PortalSpawn', @@ -7797,6 +7983,40 @@ speechSynthesis.getVoices(); this.addGameLogPyPyDance(gameLog, location); } return; + case 'photon-event': + if (!this.isGameRunning || !this.friendLogInitStatus) { + return; + } + try { + var data = JSON.parse(gameLog.json); + } catch { + console.error('error parsing photon json:', gameLog.json); + return; + } + this.parsePhotonEvent(data, gameLog.dt); + return; + case 'photon-id': + if (!this.isGameRunning || !this.friendLogInitStatus) { + return; + } + var photonId = parseInt(gameLog.photonId, 10); + var ref = this.photonLobby.get(photonId); + if (typeof ref === 'undefined') { + for (var ctx of API.cachedUsers.values()) { + if (ctx.displayName === gameLog.displayName) { + this.photonLobby.set(photonId, ctx); + this.photonLobbyCurrent.set(photonId, ctx); + return; + } + } + var ctx = { + displayName: gameLog.displayName + }; + this.photonLobby.set(photonId, ctx); + this.photonLobbyCurrent.set(photonId, ctx); + this.getCurrentInstanceUserList(); + } + return; case 'notification': // var entry = { // created_at: gameLog.dt, @@ -7819,6 +8039,851 @@ speechSynthesis.getVoices(); } }; + $app.data.lastPortalId = ''; + $app.data.lastPortalList = new Map(); + $app.data.portalQueue = ''; + $app.data.moderationEventQueue = new Map(); + $app.data.moderationAgainstTable = []; + $app.data.photonLobby = new Map(); + $app.data.photonLobbyCurrent = new Map(); + $app.data.photonLobbyAvatars = new Map(); + $app.data.photonLobbyWatcherLoop = false; + $app.data.photonLobbyTimeout = []; + $app.data.photonLobbyJointime = new Map(); + $app.data.photonLobbyBots = []; + + $app.data.photonEventType = [ + 'MeshVisibility', + 'AnimationFloat', + 'AnimationBool', + 'AnimationTrigger', + 'AudioTrigger', + 'PlayAnimation', + 'SendMessage', + 'SetParticlePlaying', + 'TeleportPlayer', + 'RunConsoleCommand', + 'SetGameObjectActive', + 'SetWebPanelURI', + 'SetWebPanelVolume', + 'SpawnObject', + 'SendRPC', + 'ActivateCustomTrigger', + 'DestroyObject', + 'SetLayer', + 'SetMaterial', + 'AddHealth', + 'AddDamage', + 'SetComponentActive', + 'AnimationInt', + 'AnimationIntAdd', + 'AnimationIntSubtract', + 'AnimationIntMultiply', + 'AnimationIntDivide', + 'AddVelocity', + 'SetVelocity', + 'AddAngularVelocity', + 'SetAngularVelocity', + 'AddForce', + 'SetUIText', + 'CallUdonMethod' + ]; + + $app.methods.startLobbyWatcherLoop = function () { + if (!this.photonLobbyWatcherLoop) { + this.photonLobbyWatcherLoop = true; + this.photonLobbyWatcher(); + } + }; + + $app.methods.photonLobbyWatcherLoopStop = function () { + this.photonLobbyWatcherLoop = false; + this.photonLobbyTimeout = []; + this.photonLobbyBots = []; + AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); + LogWatcher.ClearEvent7(); + this.updatePhotonLobbyBotSize(0); + }; + + $app.methods.photonLobbyWatcher = function () { + if (!this.photonLobbyWatcherLoop) { + return; + } + if (this.photonLobbyCurrent.size <= 1) { + this.photonLobbyWatcherLoopStop(); + return; + } + var dtNow = Date.now(); + var bias = this.lastLocationDestinationTime + 5 * 1000; + var bias1 = this.lastLocation.date + 30 * 1000; + if ( + dtNow < bias || + dtNow < bias1 || + this.lastLocation.playerList.size <= 1 + ) { + setTimeout(() => this.photonLobbyWatcher(), 500); + return; + } + LogWatcher.GetEvent7().then((event7List) => { + var hudTimeout = []; + Object.entries(event7List).forEach(([photonId, dt]) => { + var id = parseInt(photonId, 10); + var timeSinceLastEvent = dtNow - Date.parse(dt); + if (timeSinceLastEvent > this.photonLobbyTimeoutThreshold) { + var joinTime = this.photonLobbyJointime.get(id); + if (!joinTime || joinTime + 60000 < dtNow) { + // wait 1min for user to load in + var displayName = ''; + var userId = ''; + var ref = this.photonLobby.get(id); + displayName = `ID:${id}`; + if (typeof ref !== 'undefined') { + if (typeof ref.displayName !== 'undefined') { + displayName = ref.displayName; + } + if (typeof ref.id !== 'undefined') { + userId = ref.id; + } + } + var time = Math.round(timeSinceLastEvent / 1000); + var feed = { + userId, + displayName, + time + }; + hudTimeout.unshift(feed); + } + } + }); + if (this.photonLobbyTimeout.length > 0 || hudTimeout.length > 0) { + hudTimeout.sort(function (a, b) { + if (a.time > b.time) { + return 1; + } + if (a.time < b.time) { + return -1; + } + return 0; + }); + if (this.timeoutHudOverlay) { + if ( + this.timeoutHudOverlayFilter === 'VIP' || + this.timeoutHudOverlayFilter === 'Friends' + ) { + var filteredHudTimeout = []; + hudTimeout.forEach((item) => { + if ( + this.timeoutHudOverlayFilter === 'VIP' && + API.cachedFavoritesByObjectId.has(item.userId) + ) { + filteredHudTimeout.push(item); + } else if ( + this.timeoutHudOverlayFilter === 'Friends' && + this.friends.has(item.userId) + ) { + filteredHudTimeout.push(item); + } + }); + AppApi.ExecuteVrOverlayFunction( + 'updateHudTimeout', + JSON.stringify(filteredHudTimeout) + ); + } else { + AppApi.ExecuteVrOverlayFunction( + 'updateHudTimeout', + JSON.stringify(hudTimeout) + ); + } + } + this.photonLobbyTimeout = hudTimeout; + } + this.photonBotCheck(event7List); + }); + setTimeout(() => this.photonLobbyWatcher(), 500); + }; + + $app.methods.photonBotCheck = function (event7List) { + var dtNow = Date.now(); + var event7PhotonIds = Object.keys(event7List); + var photonBots = []; + var currentUserPresent = false; + this.photonLobbyCurrent.forEach((ref, id) => { + if (typeof ref !== 'undefined' && ref.id === API.currentUser.id) { + currentUserPresent = true; + } + var joinTime = this.photonLobbyJointime.get(id); + if ( + (!joinTime || joinTime + 3000 < dtNow) && + typeof ref === 'undefined' && + !event7PhotonIds.includes(id.toString()) + ) { + photonBots.unshift(id); + } + }); + if (this.photonLobbyBots.length !== photonBots.length) { + // bad bug fix is bad + if (!currentUserPresent) { + console.log('current user missing from photon lobby'); + if ( + this.lastLocation.playerList.has( + API.currentUser.displayName + ) && + photonBots.length === 1 + ) { + var ref = API.cachedUsers.get(API.currentUser.id); + if (typeof ref !== 'undefined') { + this.photonLobby.set(photonBots[0], ref); + this.photonLobbyCurrent.set(photonBots[0], ref); + } + } + return; + } + this.updatePhotonLobbyBotSize(photonBots.length); + if (photonBots.length > 0) { + var text = `photonBotIds: ${photonBots.toString()}`; + } else { + var text = 'photonBotIds: 0'; + } + this.addEntryPhotonEvent({ + photonId: '', + displayName: '', + userId: '', + text, + created_at: new Date().toJSON() + }); + } + this.photonLobbyBots = photonBots; + }; + + $app.methods.updatePhotonLobbyBotSize = function (size) { + AppApi.ExecuteVrFeedFunction('updatePhotonLobbyBotSize', `${size}`); + }; + + $app.data.photonEventTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 10, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [5, 10, 15, 25, 50] + } + }; + + $app.methods.addEntryPhotonEvent = function (feed) { + this.photonEventTable.data.unshift(feed); + if (this.photonEventOverlay) { + if ( + this.photonEventOverlayFilter === 'VIP' || + this.photonEventOverlayFilter === 'Friends' + ) { + if ( + feed.userId && + ((this.photonEventOverlayFilter === 'VIP' && + API.cachedFavoritesByObjectId.has(feed.userId)) || + (this.photonEventOverlayFilter === 'Friends' && + this.friends.has(feed.userId))) + ) { + AppApi.ExecuteVrOverlayFunction( + 'addEntryHudFeed', + JSON.stringify(feed) + ); + } + } else { + AppApi.ExecuteVrOverlayFunction( + 'addEntryHudFeed', + JSON.stringify(feed) + ); + } + } + }; + + $app.methods.getDisplayNameFromPhotonId = function (photonId) { + var displayName = ''; + if (photonId) { + var ref = this.photonLobby.get(photonId); + displayName = `ID:${photonId}`; + if ( + typeof ref !== 'undefined' && + typeof ref.displayName !== 'undefined' + ) { + displayName = ref.displayName; + } + } + return displayName; + }; + + $app.methods.showUserFromPhotonId = function (photonId) { + if (photonId) { + var ref = this.photonLobby.get(photonId); + if (typeof ref !== 'undefined') { + if (typeof ref.id !== 'undefined') { + this.showUserDialog(ref.id); + } else if (typeof ref.displayName !== 'undefined') { + this.lookupUser(ref); + } + } + } + }; + + $app.methods.getPhotonIdFromDisplayName = function (displayName) { + var photonId = ''; + if (displayName) { + this.photonLobbyCurrent.forEach((ref, id) => { + if ( + typeof ref !== 'undefined' && + ref.displayName === displayName + ) { + photonId = id; + } + }); + } + return photonId; + }; + + $app.methods.sortPhotonId = function (a, b, field) { + var id1 = this.getPhotonIdFromDisplayName(a[field]); + var id2 = this.getPhotonIdFromDisplayName(b[field]); + if (id1 < id2) { + return 1; + } + if (id1 > id2) { + return -1; + } + return 0; + }; + + $app.methods.parsePhotonEvent = function (data, gameLogDate) { + if (data.Code === 226) { + // nothing + } else if (data.Code === 253) { + // SetUserProperties + this.parsePhotonUser( + data.Parameters[253], + data.Parameters[251].user, + gameLogDate + ); + this.parsePhotonAvatarChange( + data.Parameters[253], + data.Parameters[251].user, + data.Parameters[251].avatarDict, + gameLogDate + ); + this.parsePhotonAvatar(data.Parameters[251].avatarDict); + this.parsePhotonAvatar(data.Parameters[251].favatarDict); + } else if (data.Code === 255) { + // Join + if (typeof data.Parameters[249] !== 'undefined') { + this.parsePhotonUser( + data.Parameters[254], + data.Parameters[249].user, + gameLogDate + ); + this.parsePhotonAvatarChange( + data.Parameters[254], + data.Parameters[249].user, + data.Parameters[249].avatarDict, + gameLogDate + ); + this.parsePhotonAvatar(data.Parameters[249].avatarDict); + this.parsePhotonAvatar(data.Parameters[249].favatarDict); + } + this.parsePhotonLobbyIds(data.Parameters[252].$values); + this.photonLobbyJointime.set( + data.Parameters[254], + Date.parse(gameLogDate) + ); + this.startLobbyWatcherLoop(); + } else if (data.Code === 254) { + // Leave + this.photonLobbyCurrent.delete(data.Parameters[254]); + this.photonLobbyJointime.delete(data.Parameters[254]); + this.parsePhotonLobbyIds(data.Parameters[252].$values); + } else if (data.Code === 33) { + // Moderation + if (data.Parameters[245]['0'] === 21) { + if (data.Parameters[245]['1']) { + var photonId = data.Parameters[245]['1']; + var block = data.Parameters[245]['10']; + var mute = data.Parameters[245]['11']; + var ref = this.photonLobby.get(photonId); + if ( + typeof ref !== 'undefined' && + typeof ref.id !== 'undefined' + ) { + this.photonModerationUpdate( + ref, + block, + mute, + gameLogDate + ); + } else { + this.moderationEventQueue.set(photonId, { + block, + mute, + gameLogDate + }); + if (block || mute) { + var displayName = `ID:${photonId}`; + var userId = ''; + if (typeof ref !== 'undefined') { + if (typeof ref.displayName !== 'undefined') { + displayName = ref.displayName; + } + if (typeof ref.id !== 'undefined') { + userId = ref.id; + } + } + this.addEntryPhotonEvent({ + photonId, + displayName, + userId, + text: `mute:${mute} block:${block}`, + created_at: gameLogDate + }); + } + } + } else { + var blockArray = data.Parameters[245]['10'].$values; + var muteArray = data.Parameters[245]['11'].$values; + var idList = new Map(); + blockArray.forEach((photonId1) => { + if (muteArray.includes(photonId1)) { + idList.set(photonId1, {mute: true, block: true}); + } else { + idList.set(photonId1, {mute: false, block: true}); + } + }); + muteArray.forEach((photonId2) => { + if (!idList.has(photonId2)) { + idList.set(photonId2, {mute: true, block: false}); + } + }); + idList.forEach(({isMute, isBlock}, photonId3) => { + var ref1 = this.photonLobby.get(photonId3); + if ( + typeof ref1 !== 'undefined' && + typeof ref1.id !== 'undefined' + ) { + this.photonModerationUpdate( + ref1, + isBlock, + isMute, + gameLogDate + ); + } else { + this.moderationEventQueue.set(photonId3, { + block: isBlock, + mute: isMute, + gameLogDate + }); + } + }); + } + } + } else if (data.Code === 202) { + // Instantiate + if (!this.photonLobby.has(data.Parameters[254])) { + this.photonLobby.set(data.Parameters[254]); + } + if (!this.photonLobbyCurrent.has(data.Parameters[254])) { + this.photonLobbyCurrent.set(data.Parameters[254]); + } + } else if (data.Code === 6) { + var senderId = data.Parameters[254]; + // VRC Event + if ( + data.EventType === 'ReceiveVoiceStatsSyncRPC' || + data.EventType === 'initUSpeakSenderRPC' || + data.EventType === 'SanityCheck' || + (data.EventType === 'UdonSyncRunProgramAsRPC' && + data.Data[0] !== 'Beep') || + data.EventType === 'InformOfBadConnection' || + data.EventType === 'SetTimerRPC' || + data.EventType === 'IncrementPortalPlayerCountRPC' || + data.EventType === 'PlayEffect' || + data.EventType === 'PlayEmoteRPC' || + data.EventType === 'CancelRPC' || + data.EventType === '_SendOnSpawn' || + data.EventType === 'RefreshAvatar' || + data.EventType === 'InternalApplyOverrideRPC' + ) { + return; + } + var displayName = ''; + var userId = ''; + if (senderId) { + var ref = this.photonLobby.get(senderId); + displayName = `ID:${senderId}`; + if (typeof ref !== 'undefined') { + if (typeof ref.displayName !== 'undefined') { + displayName = ref.displayName; + } + if (typeof ref.id !== 'undefined') { + userId = ref.id; + } + } + } + if ( + data.EventType === '_InstantiateObject' && + data.Data[0] === 'Portals/PortalInternalDynamic' + ) { + this.lastPortalId = data.Data[3]; + return; + } else if ( + data.EventType === '_DestroyObject' && + this.lastPortalList.has(data.Data[0]) + ) { + var portalId = data.Data[0]; + var date = this.lastPortalList.get(portalId); + var time = timeToText(Date.parse(gameLogDate) - date); + this.addEntryPhotonEvent({ + photonId: senderId, + displayName, + userId, + text: `DeletedPortal ${time}`, + created_at: gameLogDate + }); + return; + } else if (data.EventType === 'ConfigurePortal') { + var instanceId = `${data.Data[0]}:${data.Data[1]}`; + if (this.lastPortalId) { + this.lastPortalList.set( + this.lastPortalId, + Date.parse(gameLogDate) + ); + this.lastPortalId = ''; + } + var ref = this.photonLobby.get(senderId); + if ( + typeof ref !== 'undefined' && + typeof ref.displayName !== 'undefined' + ) { + var userId = ''; + if (typeof ref.id !== 'undefined') { + userId = ref.id; + } + var ref1 = { + id: userId, + displayName: ref.displayName + }; + this.portalQueue = 'skip'; + this.parsePhotonPortalSpawn(gameLogDate, instanceId, ref1); + } else { + this.portalQueue = instanceId; + } + return; + } else if (data.Type > 34) { + var entry = { + created_at: gameLogDate, + type: 'Event', + data: `${displayName} called non existent RPC ${data.Type}` + }; + this.addPhotonEventToGameLog(entry); + } + if (data.Type === 14) { + if (data.EventType === 'ChangeVisibility') { + if (data.Data[0] === true) { + var text = 'EnableCamera'; + } else if (data.Data[0] === false) { + var text = 'DisableCamera'; + } + } else if ( + data.EventType === 'UdonSyncRunProgramAsRPC' && + data.Data[0] === 'Beep' + ) { + var L = API.parseLocation(this.lastLocation.location); + if ( + L.worldId !== + 'wrld_f20326da-f1ac-45fc-a062-609723b097b1' + ) { + return; + } + var text = 'Beep'; + } else if (data.EventType === 'ReloadAvatarNetworkedRPC') { + var text = 'AvatarReset'; + } else { + var eventData = ''; + if (data.Data) { + if (Array.isArray(data.Data)) { + eventData = ` ${data.Data.toString()}`; + } else { + eventData = ` ${data.Data}`; + } + } + var text = `${data.EventType}${eventData}`; + } + this.addEntryPhotonEvent({ + photonId: senderId, + displayName, + userId, + text, + created_at: gameLogDate + }); + } else { + var eventType = ''; + if (data.EventType) { + if (Array.isArray(data.EventType)) { + eventType = ` ${data.EventType.toString()}`; + } else { + eventType = ` ${data.EventType}`; + } + } + var feed = `RPC ${displayName} ${ + this.photonEventType[data.Type] + }${eventType}`; + console.log(feed); + } + } + }; + + $app.methods.parsePhotonPortalSpawn = async function ( + created_at, + instanceId, + ref + ) { + var L = API.parseLocation(instanceId); + var args = await API.getCachedWorld({ + worldId: L.worldId + }); + this.addPhotonEventToGameLog({ + created_at, + type: 'PortalSpawn', + displayName: ref.displayName, + location: this.lastLocation.location, + userId: ref.id, + instanceId, + worldName: args.ref.name + }); + }; + + $app.methods.addPhotonEventToGameLog = function (entry) { + this.queueGameLogNoty(entry); + this.addGameLog(entry); + if (entry.type === 'PortalSpawn') { + database.addGamelogPortalSpawnToDatabase(entry); + } else if (entry.type === 'Event') { + database.addGamelogEventToDatabase(entry); + } + }; + + $app.methods.parsePhotonLobbyIds = function (lobbyIds) { + lobbyIds.forEach((id) => { + if (!this.photonLobby.has(id)) { + this.photonLobby.set(id); + } + if (!this.photonLobbyCurrent.has(id)) { + this.photonLobbyCurrent.set(id); + } + }); + for (var id of this.photonLobbyCurrent.keys()) { + if (!lobbyIds.includes(id)) { + this.photonLobbyCurrent.delete(id); + } + } + }; + + $app.methods.parsePhotonUser = async function ( + photonId, + user, + gameLogDate + ) { + var tags = []; + if ( + typeof user.tags !== 'undefined' && + typeof user.tags.$values !== 'undefined' + ) { + tags = user.tags.$values; + } + var photonUser = { + id: user.id, + username: user.username, + displayName: user.displayName, + developerType: user.developerType, + profilePicOverride: user.profilePicOverride, + currentAvatarImageUrl: user.currentAvatarImageUrl, + currentAvatarThumbnailImageUrl: user.currentAvatarThumbnailImageUrl, + userIcon: user.userIcon, + last_platform: user.last_platform, + allowAvatarCopying: user.allowAvatarCopying, + status: user.status, + statusDescription: user.statusDescription, + bio: user.bio, + tags + }; + this.photonLobby.set(photonId, photonUser); + this.photonLobbyCurrent.set(photonId, photonUser); + var ref = API.cachedUsers.get(user.id); + var bias = Date.parse(gameLogDate) + 60 * 1000; // 1min + if (bias > Date.now()) { + if (typeof ref === 'undefined' || typeof ref.id === 'undefined') { + var args = await API.getUser({ + userId: user.id + }); + ref = args.ref; + } else if ( + !ref.isFriend && + this.lastLocation.playerList.has(ref.displayName) + ) { + var {joinTime} = this.lastLocation.playerList.get( + ref.displayName + ); + if (!joinTime) { + joinTime = Date.parse(gameLogDate); + } + ref.$location_at = joinTime; + ref.$online_for = joinTime; + } + if (ref.currentAvatarImageUrl !== user.currentAvatarImageUrl) { + if (typeof ref.id !== 'undefined') { + API.applyUser({ + ...ref, + currentAvatarImageUrl: user.currentAvatarImageUrl, + currentAvatarThumbnailImageUrl: + user.currentAvatarThumbnailImageUrl + }); + } + } + } + if (typeof ref !== 'undefined' && typeof ref.id !== 'undefined') { + this.photonLobby.set(photonId, ref); + this.photonLobbyCurrent.set(photonId, ref); + // check moderation queue + if (this.moderationEventQueue.has(photonId)) { + var {block, mute, gameLogDate} = + this.moderationEventQueue.get(photonId); + this.moderationEventQueue.delete(photonId); + this.photonModerationUpdate(ref, block, mute, gameLogDate); + } + } + }; + + $app.methods.photonModerationUpdate = function ( + ref, + block, + mute, + gameLogDate + ) { + database.getModeration(ref.id).then((row) => { + var type = ''; + if (block) { + type = 'Blocked'; + } else if (mute) { + type = 'Muted'; + } + if (row.userId) { + if (block === row.block && mute === row.mute) { + return; + } + if (!block && row.block) { + type = 'Unblocked'; + } else if (!mute && row.mute) { + type = 'Unmuted'; + } + } + if (type) { + var noty = { + created_at: new Date().toJSON(), + userId: ref.id, + displayName: ref.displayName, + type + }; + this.queueModerationNoty(noty); + var entry = { + created_at: gameLogDate, + userId: ref.id, + displayName: ref.displayName, + type + }; + this.moderationAgainstTable.forEach((item) => { + if (item.userId === ref.id && item.type === type) { + removeFromArray(this.moderationAgainstTable, item); + } + }); + this.moderationAgainstTable.push(entry); + this.updateSharedFeed(true); + } + if (block || mute) { + database.setModeration({ + userId: ref.id, + updatedAt: gameLogDate, + displayName: ref.displayName, + block, + mute + }); + } else { + database.deleteModeration(ref.id); + } + }); + }; + + $app.methods.parsePhotonAvatarChange = function ( + photonId, + user, + avatar, + gameLogDate + ) { + var oldAvatarId = this.photonLobbyAvatars.get(user.id); + if ( + oldAvatarId && + oldAvatarId !== avatar.id && + user.id !== API.currentUser.id + ) { + var entry = { + created_at: new Date().toJSON(), + type: 'AvatarChange', + userId: user.id, + displayName: user.displayName, + name: avatar.name, + description: avatar.description, + avatarId: avatar.id, + authorId: avatar.authorId, + releaseStatus: avatar.releaseStatus, + imageUrl: avatar.imageUrl, + thumbnailImageUrl: avatar.thumbnailImageUrl + }; + this.queueGameLogNoty(entry); + this.addGameLog(entry); + this.addEntryPhotonEvent({ + photonId, + displayName: user.displayName, + userId: user.id, + text: `ChangeAvatar ${avatar.name}`, + created_at: gameLogDate, + avatar + }); + } + this.photonLobbyAvatars.set(user.id, avatar.id); + }; + + $app.methods.parsePhotonAvatar = function (avatar) { + var tags = []; + if ( + typeof avatar.tags !== 'undefined' && + typeof avatar.tags.$values !== 'undefined' + ) { + tags = avatar.tags.$values; + } + API.applyAvatar({ + id: avatar.id, + assetUrl: avatar.assetUrl, // remove this + authorId: avatar.authorId, + authorName: avatar.authorName, + updated_at: avatar.updated_at, + description: avatar.description, + featured: avatar.featured, + imageUrl: avatar.imageUrl, + thumbnailImageUrl: avatar.thumbnailImageUrl, + name: avatar.name, + releaseStatus: avatar.releaseStatus, + version: avatar.version, + tags + }); + }; + $app.methods.addGameLogVideo = async function (gameLog, location, userId) { var videoUrl = gameLog.videoUrl; var youtubeVideoId = ''; @@ -7857,6 +8922,10 @@ speechSynthesis.getVoices(); videoLength = this.convertYoutubeTime( data.items[0].contentDetails.duration ); + if (videoLength) { + // add loading time + videoLength += 15; + } } } } catch { @@ -9443,6 +10512,18 @@ speechSynthesis.getVoices(); }, layout: 'table' }; + $app.data.currentInstanceUserList = { + data: [], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'photonId', + order: 'descending' + } + }, + layout: 'table' + }; $app.data.visits = 0; $app.data.openVR = configRepository.getBool('openVR'); $app.data.openVRAlways = configRepository.getBool('openVRAlways'); @@ -9503,6 +10584,9 @@ speechSynthesis.getVoices(); $app.data.branch = configRepository.getString('VRCX_branch'); $app.data.maxTableSize = configRepository.getInt('VRCX_maxTableSize'); database.setmaxTableSize($app.data.maxTableSize); + $app.data.photonLobbyTimeoutThreshold = configRepository.getString( + 'VRCX_photonLobbyTimeoutThreshold' + ); $app.methods.saveOpenVROption = function () { configRepository.setBool('openVR', this.openVR); configRepository.setBool('openVRAlways', this.openVRAlways); @@ -9625,6 +10709,39 @@ speechSynthesis.getVoices(); $app.watch.isStartAtWindowsStartup = saveVRCXWindowOption; $app.watch.isStartAsMinimizedState = saveVRCXWindowOption; $app.watch.isCloseToTray = saveVRCXWindowOption; + $app.data.photonEventOverlay = configRepository.getBool( + 'VRCX_PhotonEventOverlay' + ); + $app.data.timeoutHudOverlay = configRepository.getBool( + 'VRCX_TimeoutHudOverlay' + ); + $app.data.timeoutHudOverlayFilter = configRepository.getString( + 'VRCX_TimeoutHudOverlayFilter' + ); + $app.data.photonEventOverlayFilter = configRepository.getString( + 'VRCX_PhotonEventOverlayFilter' + ); + $app.methods.saveEventOverlay = function () { + configRepository.setBool( + 'VRCX_PhotonEventOverlay', + this.photonEventOverlay + ); + configRepository.setBool( + 'VRCX_TimeoutHudOverlay', + this.timeoutHudOverlay + ); + configRepository.setString( + 'VRCX_TimeoutHudOverlayFilter', + this.timeoutHudOverlayFilter + ); + configRepository.setString( + 'VRCX_PhotonEventOverlayFilter', + this.photonEventOverlayFilter + ); + if (!this.timeoutHudOverlay) { + AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); + } + }; // setting defaults if (!configRepository.getString('VRCX_notificationPosition')) { @@ -9710,6 +10827,27 @@ speechSynthesis.getVoices(); configRepository.getInt('VRCX_maxTableSize', $app.data.maxTableSize); database.setmaxTableSize($app.data.maxTableSize); } + if (!configRepository.getString('VRCX_photonLobbyTimeoutThreshold')) { + $app.data.photonLobbyTimeoutThreshold = 3000; + configRepository.setString( + 'VRCX_photonLobbyTimeoutThreshold', + $app.data.photonLobbyTimeoutThreshold + ); + } + if (!configRepository.getString('VRCX_TimeoutHudOverlayFilter')) { + $app.data.timeoutHudOverlayFilter = 'Everyone'; + configRepository.setString( + 'VRCX_TimeoutHudOverlayFilter', + $app.data.timeoutHudOverlayFilter + ); + } + if (!configRepository.getString('VRCX_PhotonEventOverlayFilter')) { + $app.data.photonEventOverlayFilter = 'Everyone'; + configRepository.setString( + 'VRCX_PhotonEventOverlayFilter', + $app.data.photonEventOverlayFilter + ); + } if (!configRepository.getString('sharedFeedFilters')) { var sharedFeedFilters = { noty: { @@ -9731,13 +10869,17 @@ speechSynthesis.getVoices(); DisplayName: 'VIP', TrustLevel: 'VIP', PortalSpawn: 'Everyone', - AvatarChange: 'Off', Event: 'On', VideoPlay: 'Off', BlockedOnPlayerJoined: 'Off', BlockedOnPlayerLeft: 'Off', MutedOnPlayerJoined: 'Off', - MutedOnPlayerLeft: 'Off' + MutedOnPlayerLeft: 'Off', + AvatarChange: 'Off', + Blocked: 'Off', + Unblocked: 'Off', + Muted: 'Off', + Unmuted: 'Off' }, wrist: { Location: 'On', @@ -9758,13 +10900,17 @@ speechSynthesis.getVoices(); DisplayName: 'Friends', TrustLevel: 'Friends', PortalSpawn: 'Everyone', - AvatarChange: 'Everyone', Event: 'On', VideoPlay: 'On', BlockedOnPlayerJoined: 'Off', BlockedOnPlayerLeft: 'Off', MutedOnPlayerJoined: 'Off', - MutedOnPlayerLeft: 'Off' + MutedOnPlayerLeft: 'Off', + AvatarChange: 'Everyone', + Blocked: 'On', + Unblocked: 'On', + Muted: 'On', + Unmuted: 'On' } }; configRepository.setString( @@ -9775,6 +10921,16 @@ speechSynthesis.getVoices(); $app.data.sharedFeedFilters = JSON.parse( configRepository.getString('sharedFeedFilters') ); + if (!$app.data.sharedFeedFilters.noty.Blocked) { + $app.data.sharedFeedFilters.noty.Blocked = 'Off'; + $app.data.sharedFeedFilters.noty.Unblocked = 'Off'; + $app.data.sharedFeedFilters.noty.Muted = 'Off'; + $app.data.sharedFeedFilters.noty.Unmuted = 'Off'; + $app.data.sharedFeedFilters.wrist.Blocked = 'On'; + $app.data.sharedFeedFilters.wrist.Unblocked = 'On'; + $app.data.sharedFeedFilters.wrist.Muted = 'On'; + $app.data.sharedFeedFilters.wrist.Unmuted = 'On'; + } if (!configRepository.getString('VRCX_trustColor')) { configRepository.setString( @@ -9942,6 +11098,7 @@ speechSynthesis.getVoices(); $app.methods.vrInit = function () { downloadProgressStateChange(); + this.updatePhotonLobbyBotSize(this.photonLobbyBots.length); this.updateVRConfigVars(); this.updateVRLastLocation(); this.updateVrNowPlaying(); @@ -10009,7 +11166,12 @@ speechSynthesis.getVoices(); (isGameRunning || this.openVRAlways) ) { var hmdOverlay = false; - if (this.overlayNotifications || this.progressPie) { + if ( + this.overlayNotifications || + this.progressPie || + this.photonEventOverlay || + this.timeoutHudOverlay + ) { hmdOverlay = true; } // active, hmdOverlay, wristOverlay @@ -10500,6 +11662,7 @@ speechSynthesis.getVoices(); ); database.setmaxTableSize(this.maxTableSize); this.feedTableLookup(); + this.gameLogTableLookup(); } } }); @@ -10515,6 +11678,36 @@ speechSynthesis.getVoices(); configRepository.setInt('VRCX_tablePageSize', pageSize); }; + $app.methods.promptPhotonLobbyTimeoutThreshold = function () { + this.$prompt( + 'Enter amount of seconds (default: 3)', + 'Photon Lobby Timeout Threshold', + { + distinguishCancelAndClose: true, + confirmButtonText: 'OK', + cancelButtonText: 'Cancel', + inputValue: this.photonLobbyTimeoutThreshold / 1000, + inputPattern: /\d+$/, + inputErrorMessage: 'Valid number is required', + callback: (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue && + !isNaN(instance.inputValue) + ) { + this.photonLobbyTimeoutThreshold = Math.trunc( + Number(instance.inputValue) * 1000 + ); + configRepository.setString( + 'VRCX_photonLobbyTimeoutThreshold', + this.photonLobbyTimeoutThreshold + ); + } + } + } + ); + }; + // App: Dialog var adjustDialogZ = (el) => { @@ -10968,6 +12161,179 @@ speechSynthesis.getVoices(); D.instance.friendCount = friendCount; }; + // App: player list + + API.$on('LOGIN', function () { + $app.currentInstanceUserList.data = []; + }); + + API.$on('USER:APPLY', function (ref) { + // add user ref to playerList, friendList, photonLobby, photonLobbyCurrent + if ($app.lastLocation.playerList.has(ref.displayName)) { + var playerListRef = $app.lastLocation.playerList.get( + ref.displayName + ); + if (!playerListRef.userId) { + playerListRef.userId = ref.id; + $app.lastLocation.playerList.set( + ref.displayName, + playerListRef + ); + if ($app.lastLocation.friendList.has(ref.displayName)) { + $app.lastLocation.friendList.set( + ref.displayName, + playerListRef + ); + } + } + $app.photonLobby.forEach((ref1, id) => { + if ( + typeof ref1 !== 'undefined' && + ref1.displayName === ref.displayName && + ref1 !== ref + ) { + $app.photonLobby.set(id, ref); + if ($app.photonLobbyCurrent.has(id)) { + $app.photonLobbyCurrent.set(id, ref); + } + } + }); + $app.getCurrentInstanceUserList(); + } + }); + + $app.methods.getCurrentInstanceUserList = function () { + var users = []; + var pushUser = function (ref) { + var photonId = ''; + var masterId = 0; + $app.photonLobbyCurrent.forEach((ref1, id) => { + if (masterId === 0 || masterId > id) { + masterId = id; + } + if (typeof ref1 !== 'undefined') { + if ( + (typeof ref.id !== 'undefined' && + typeof ref1.id !== 'undefined' && + ref1.id === ref.id) || + (typeof ref.displayName !== 'undefined' && + typeof ref1.displayName !== 'undefined' && + ref1.displayName === ref.displayName) + ) { + photonId = id; + } + } + }); + var isMaster = false; + if (photonId === masterId) { + isMaster = true; + } + users.push({ + ref, + timer: ref.$location_at, + photonId, + isMaster + }); + // get block, mute + }; + + var playersInInstance = this.lastLocation.playerList; + if (playersInInstance.size > 0) { + var ref = API.cachedUsers.get(API.currentUser.id); + if (typeof ref === 'undefined') { + ref = API.currentUser; + } + if (playersInInstance.has(ref.displayName)) { + pushUser(ref); + } + for (var player of playersInInstance.values()) { + // if friend isn't in instance add them + if (player.displayName === API.currentUser.displayName) { + continue; + } + var addUser = true; + for (var k = 0; k < users.length; k++) { + var user = users[k]; + if (player.displayName === user.displayName) { + addUser = false; + break; + } + } + if (addUser) { + var ref = API.cachedUsers.get(player.userId); + if (typeof ref !== 'undefined') { + if ( + !ref.isFriend || + ref.status === 'ask me' || + ref.status === 'busy' + ) { + // fix $location_at + var {joinTime} = this.lastLocation.playerList.get( + ref.displayName + ); + if (!joinTime) { + joinTime = Date.now(); + } + ref.$location_at = joinTime; + } + pushUser(ref); + } else { + var {joinTime} = this.lastLocation.playerList.get( + player.displayName + ); + if (!joinTime) { + joinTime = Date.now(); + } + var ref = { + // if userId is missing just push displayName + displayName: player.displayName, + $location_at: joinTime, + $online_for: joinTime + }; + pushUser(ref); + } + } + } + } + this.currentInstanceUserList.data = users; + this.updateTimers(); + }; + + $app.data.currentInstanceWorld = {}; + $app.data.currentInstanceLocation = {}; + + $app.methods.updateCurrentInstanceWorld = function (instanceId) { + this.currentInstanceWorld = {}; + this.currentInstanceLocation = {}; + if (instanceId) { + var L = API.parseLocation(instanceId); + this.currentInstanceLocation = L; + API.getCachedWorld({ + worldId: L.worldId + }).then((args) => { + this.currentInstanceWorld = args.ref; + }); + } + }; + + $app.methods.selectCurrentInstanceRow = function (val) { + if (val === null) { + return; + } + var ref = val.ref; + if (ref.id) { + this.showUserDialog(ref.id); + } else { + this.lookupUser(ref); + } + }; + + $app.methods.updateTimers = function () { + for (var $timer of $timers) { + $timer.update(); + } + }; + $app.methods.setUserDialogWorlds = function (userId) { var worlds = []; for (var ref of API.cachedWorlds.values()) { diff --git a/html/src/app.scss b/html/src/app.scss index 69548a66..af9930f9 100644 --- a/html/src/app.scss +++ b/html/src/app.scss @@ -82,10 +82,14 @@ margin-top: 15px; } -.el-table__expanded-cell[class*='cell'] { +.el-table--mini .el-table__expanded-cell[class*='cell'] { padding: 20px 50px; } +.el-table--mini .el-table__cell { + padding: 5px 0; +} + .el-dialog__body { padding: 20px; } @@ -615,3 +619,20 @@ i.x-user-status.busy { .el-form-item { margin-bottom: 4px; } + +.photon-event-table .el-table--mini .el-table__cell, +.current-instance-table .el-table--mini .el-table__cell { + padding: 0; +} + +.photon-event-table { + margin-top: 20px; +} + +.current-instance-table img.friends-list-avatar { + width: unset; + height: 16px; + margin-right: 0; + margin-left: 3px; + border-radius: 2px; +} diff --git a/html/src/constants.js b/html/src/constants.js index 9a0e642f..756221a4 100644 --- a/html/src/constants.js +++ b/html/src/constants.js @@ -1 +1 @@ -export var appVersion = 'VRCX 2021.11.04'; +export var appVersion = 'VRCX 2021.11.20'; diff --git a/html/src/index.pug b/html/src/index.pug index 472e2b9d..cc90e47b 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -59,6 +59,7 @@ html span= name +menuitem('feed', 'Feed', 'el-icon-news') +menuitem('gameLog', 'Game Log', 'el-icon-s-data') + +menuitem('playerList', 'Player List', 'el-icon-tickets') +menuitem('search', 'Search', 'el-icon-search') +menuitem('favorite', 'Favorite', 'el-icon-star-off') +menuitem('friendLog', 'Friend Log', 'el-icon-notebook-2') @@ -68,6 +69,98 @@ html +menuitem('profile', 'Profile', 'el-icon-user') +menuitem('settings', 'Settings', 'el-icon-s-tools') + //- playerList + .x-container(v-show="$refs.menu && $refs.menu.activeIndex === 'playerList'") + div(style="display:flex;flex-direction:column;height:100%") + div(v-if="currentInstanceWorld.id" style="display:flex") + el-popover(placement="right" width="500px" trigger="click" style="height:120px") + img.x-link(slot="reference" v-lazy="currentInstanceWorld.thumbnailImageUrl" style="flex:none;width:160px;height:120px;border-radius:4px") + img.x-link(v-lazy="currentInstanceWorld.imageUrl" style="width:500px;height:375px" @click="openExternalLink(currentInstanceWorld.imageUrl)") + div(style="margin-left:10px;display:flex;flex-direction:column") + div + span.x-link(@click="showWorldDialog(currentInstanceWorld.id)" style="font-weight:bold;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1") + | #[i.el-icon-s-home(v-show="API.currentUser.$homeLocation && API.currentUser.$homeLocation.worldId === currentInstanceWorld.id")] {{ currentInstanceWorld.name }} + div + span.x-link(v-text="currentInstanceWorld.authorName" @click="showUserDialog(currentInstanceWorld.authorId)" style="color:#909399;font-family:monospace") + div(style="margin-top:5px") + el-tag(v-if="currentInstanceWorld.$isLabs" type="primary" effect="plain" size="mini") Labs + el-tag(v-else-if="currentInstanceWorld.releaseStatus === 'public'" type="success" effect="plain" size="mini") Public + el-tag(v-else-if="currentInstanceWorld.releaseStatus === 'private'" type="danger" effect="plain" size="mini") Private + span(style="margin-left:5px") + span \#{{ currentInstanceLocation.instanceName }} {{ currentInstanceLocation.accessType }} + span.famfamfam-flags(v-if="currentInstanceLocation.region === 'eu'" class="europeanunion" style="display:inline-block;margin-left:5px") + span.famfamfam-flags(v-else-if="currentInstanceLocation.region === 'jp'" class="jp" style="display:inline-block;margin-left:5px") + span.famfamfam-flags(v-else class="us" style="display:inline-block;margin-left:5px") + span(v-if="lastLocation.playerList.size > 0" style="margin-left:5px") + | {{ lastLocation.playerList.size }} + | #[template(v-if="lastLocation.friendList.size > 0") ({{ lastLocation.friendList.size }})] + template(v-if="photonLobbyBots.length > 0") + |   + el-tooltip(placement="bottom") + template(#content) + span Photon Bot Id's: {{ photonLobbyBots.toString() }} + span(v-text="photonLobbyBots.length" style="color:red") + //- el-tag(type="info" effect="plain" size="mini" v-text="worldDialog.fileSize" style="margin-right:5px;margin-top:5px") + div(style="margin-top:5px") + span(v-show="currentInstanceWorld.name !== currentInstanceWorld.description" v-text="currentInstanceWorld.description" style="font-size:12px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2") + div.current-instance-table + data-tables(v-bind="currentInstanceUserList" @sort-change="updateTimers" @row-click="selectCurrentInstanceRow" style="margin-top:10px;cursor:pointer") + el-table-column(label="Avatar" width="60" prop="photo") + template(v-once #default="scope") + template(v-if="userImage(scope.row.ref)") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.ref)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.ref)" style="height:500px;cursor:pointer" @click="openExternalLink(userImageFull(scope.row.ref))") + el-table-column(label="Timer" width="90" prop="timer" sortable) + template(v-once #default="scope") + timer(:epoch="scope.row.timer") + el-table-column(label="Photon Id" width="100" prop="photonId" sortable) + template(v-once #default="scope") + span(v-text="scope.row.photonId") + el-table-column(label="Display Name" min-width="140" prop="ref.displayName") + template(v-once #default="scope") + span(v-text="scope.row.ref.displayName" :class="scope.row.ref.$trustColor") + el-table-column(label="Status" min-width="180" prop="ref.status") + template(v-once #default="scope") + template(v-if="scope.row.ref.status") + i.x-user-status(:class="statusClass(scope.row.ref.status)") + span ‎ + span(v-text="scope.row.ref.statusDescription") + el-table-column(label="Language" width="100" prop="ref.$languages") + template(v-once #default="scope") + el-tooltip(v-for="item in scope.row.ref.$languages" :key="item.key" placement="top") + template(#content) + span {{ item.value }} ({{ item.key }}) + span.famfamfam-flags(:class="languageClass(item.key)" style="display:inline-block;margin-left:5px") + el-table-column(label="Bio Links" width="100" prop="ref.bioLinks") + template(v-once #default="scope") + el-tooltip(v-if="link" v-for="(link, index) in scope.row.ref.bioLinks" :key="index") + template(#content) + span(v-text="link") + img(:src="getFaviconUrl(link)" style="width:16px;height:16px;vertical-align:middle;margin-right:5px;cursor:pointer" @click.stop="openExternalLink(link)") + div.photon-event-table(v-if="photonEventTable.data.length > 0") + data-tables(v-bind="photonEventTable" style="margin-bottom:10px") + el-table-column(label="Date" prop="created_at" width="90") + template(v-once #default="scope") + el-tooltip(placement="right") + template(#content) + span {{ scope.row.created_at | formatDate('YYYY-MM-DD HH24:MI:SS') }} + span {{ scope.row.created_at | formatDate('MM-DD HH24:MI') }} + el-table-column(label="Name" prop="photonId" width="160") + template(v-once #default="scope") + span.x-link(v-text="getDisplayNameFromPhotonId(scope.row.photonId)" @click="showUserFromPhotonId(scope.row.photonId)" style="padding-right:10px") + el-table-column(label="Event" prop="text") + template(v-once #default="scope") + span(v-if="scope.row.avatar") + span ChangeAvatar + span.x-link(v-text="scope.row.avatar.name" @click="showAvatarDialog(scope.row.avatar.id)") + |   + span.avatar-info-public(v-if="scope.row.avatar.releaseStatus === 'public'") (Public) + span.avatar-info-own(v-else-if="scope.row.avatar.releaseStatus === 'private'") (Private) + template(v-if="scope.row.avatar.description && scope.row.avatar.name !== scope.row.avatar.description") + | - {{ scope.row.avatar.description }} + span(v-else v-text="scope.row.text") + //- feed .x-container(v-show="$refs.menu && $refs.menu.activeIndex === 'feed'") data-tables(v-bind="feedTable" v-loading="feedTable.loading") @@ -411,7 +504,7 @@ html template(#tool) div(style="margin:0 0 10px;display:flex;align-items:center") el-select(v-model="playerModerationTable.filters[0].value" @change="saveTableFilters" multiple clearable collapse-tags style="flex:1" placeholder="Filter") - el-option(v-once v-for="type in ['block', 'mute', 'unmute', 'hideAvatar', 'showAvatar']" :key="type" :label="type" :value="type") + el-option(v-once v-for="type in ['block', 'unblock', 'mute', 'unmute', 'hideAvatar', 'showAvatar']" :key="type" :label="type" :value="type") el-input(v-model="playerModerationTable.filters[1].value" placeholder="Search" style="flex:none;width:150px;margin:0 10px") el-tooltip(placement="bottom" content="Refresh" :disabled="hideTooltips") el-button(type="default" :loading="API.isPlayerModerationsLoading" @click="API.refreshPlayerModerations()" icon="el-icon-refresh" circle style="flex:none") @@ -806,7 +899,7 @@ html span.color-picker(slot="trigger") #[i.el-icon-s-open] Veteran User div v-swatches(v-model="trustColor.legendary" show-fallback fallback-input-type="color" popover-x="right" :swatches="trustColorSwatches" class="x-tag-legendary") - span.color-picker(slot="trigger") #[i.el-icon-s-open] Early User + span.color-picker(slot="trigger") #[i.el-icon-s-open] Legend div v-swatches(v-model="trustColor.vip" show-fallback fallback-input-type="color" popover-x="right" :swatches="trustColorSwatches" class="x-tag-vip") span.color-picker(slot="trigger") #[i.el-icon-s-open] VRChat Team @@ -977,6 +1070,33 @@ html div.options-container-item span.name Dance worlds only el-switch(v-model="progressPieFilter" @change="changeYouTubeApi" :disabled="!openVR") + div.options-container + span.header Photon Logging Overlay + el-tooltip(placement="top" style="margin-left:5px" content="Requires '--log-debug-levels=API;NetworkData' steam launch option") + i.el-icon-warning + div.options-container-item + span.sub-header Photon Event HUD + div.options-container-item + span.name Enable + el-switch(v-model="photonEventOverlay" @change="saveEventOverlay" :disabled="!openVR") + div.options-container-item + span.name Filter + el-radio-group(v-model="photonEventOverlayFilter" @change="saveEventOverlay" size="mini" :disabled="!openVR || !photonEventOverlay") + el-radio-button(label="VIP") + el-radio-button(label="Friends") + el-radio-button(label="Everyone") + span.sub-header User timeout HUD + div.options-container-item + span.name Enable + el-switch(v-model="timeoutHudOverlay" @change="saveEventOverlay" :disabled="!openVR") + div.options-container-item + span.name Filter + el-radio-group(v-model="timeoutHudOverlayFilter" @change="saveEventOverlay" size="mini" :disabled="!openVR || !timeoutHudOverlay") + el-radio-button(label="VIP") + el-radio-button(label="Friends") + el-radio-button(label="Everyone") + div.options-container-item + el-button(size="small" icon="el-icon-time" @click="promptPhotonLobbyTimeoutThreshold" :disabled="!openVR") Timeout Threshold div.options-container span.header VRCX Instance Cache/Debug div.options-container-item @@ -1595,7 +1715,7 @@ html el-dialog.x-dialog(ref="favoriteDialog" :visible.sync="favoriteDialog.visible" title="Choose Group" width="300px") div(v-loading="favoriteDialog.loading") el-button(v-for="group in favoriteDialog.groups" :key="group.name" style="display:block;width:100%;margin:10px 0" @click="addFavorite(group)" :disabled="group.count >= group.capacity") {{ group.displayName }} ({{ group.count }} / {{ group.capacity }}) - + //- dialog: invite el-dialog.x-dialog(ref="inviteDialog" :visible.sync="inviteDialog.visible" title="Invite" width="450px") div(v-loading="inviteDialog.loading") @@ -2088,10 +2208,42 @@ html el-radio-button(label="VIP") el-radio-button(label="Friends") el-radio-button(label="Everyone") + br + .toggle-item + span.toggle-name Photon Event Logging + el-tooltip(placement="top" style="margin-left:5px" content="Requires '--log-debug-levels=API;NetworkData' steam launch option") + i.el-icon-warning + .toggle-item + span.toggle-name Lobby Avatar Change + el-radio-group(v-model="sharedFeedFilters.noty.AvatarChange" size="mini") + el-radio-button(label="Off") + el-radio-button(label="VIP") + el-radio-button(label="Friends") + el-radio-button(label="Everyone") + .toggle-item + span.toggle-name Blocked + el-radio-group(v-model="sharedFeedFilters.noty.Blocked" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") + .toggle-item + span.toggle-name Unblocked + el-radio-group(v-model="sharedFeedFilters.noty.Unblocked" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") + .toggle-item + span.toggle-name Muted + el-radio-group(v-model="sharedFeedFilters.noty.Muted" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") + .toggle-item + span.toggle-name Unmuted + el-radio-group(v-model="sharedFeedFilters.noty.Unmuted" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") template(#footer) el-button(type="small" @click="cancelSharedFeedFilters") Cancel el-button(type="primary" size="small" style="margin-left:10px" @click="saveSharedFeedFilters") Save - + //- dialog: wrist feed filters el-dialog.x-dialog(ref="wristFeedFiltersDialog" :visible.sync="wristFeedFiltersDialog.visible" title="Wrist Feed Filters" width="500px") .toggle-list @@ -2244,6 +2396,38 @@ html el-radio-button(label="VIP") el-radio-button(label="Friends") el-radio-button(label="Everyone") + br + .toggle-item + span.toggle-name Photon Event Logging + el-tooltip(placement="top" style="margin-left:5px" content="Requires '--log-debug-levels=API;NetworkData' steam launch option") + i.el-icon-warning + .toggle-item + span.toggle-name Lobby Avatar Change + el-radio-group(v-model="sharedFeedFilters.wrist.AvatarChange" size="mini") + el-radio-button(label="Off") + el-radio-button(label="VIP") + el-radio-button(label="Friends") + el-radio-button(label="Everyone") + .toggle-item + span.toggle-name Blocked + el-radio-group(v-model="sharedFeedFilters.wrist.Blocked" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") + .toggle-item + span.toggle-name Unblocked + el-radio-group(v-model="sharedFeedFilters.wrist.Unblocked" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") + .toggle-item + span.toggle-name Muted + el-radio-group(v-model="sharedFeedFilters.wrist.Muted" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") + .toggle-item + span.toggle-name Unmuted + el-radio-group(v-model="sharedFeedFilters.wrist.Unmuted" size="mini") + el-radio-button(label="Off") + el-radio-button(label="On") template(#footer) el-button(type="small" @click="cancelSharedFeedFilters") Cancel el-button(type="primary" size="small" @click="saveSharedFeedFilters") Save diff --git a/html/src/repository/database.js b/html/src/repository/database.js index 23c9612d..cbd53a3c 100644 --- a/html/src/repository/database.js +++ b/html/src/repository/database.js @@ -29,6 +29,9 @@ class Database { await sqliteService.executeNonQuery( `CREATE TABLE IF NOT EXISTS ${Database.userPrefix}_notifications (id TEXT PRIMARY KEY, created_at TEXT, type TEXT, sender_user_id TEXT, sender_username TEXT, receiver_user_id TEXT, message TEXT, world_id TEXT, world_name TEXT, image_url TEXT, invite_message TEXT, request_message TEXT, response_message TEXT, expired INTEGER)` ); + await sqliteService.executeNonQuery( + `CREATE TABLE IF NOT EXISTS ${Database.userPrefix}_moderation (user_id TEXT PRIMARY KEY, updated_at TEXT, display_name TEXT, block INTEGER, mute INTEGER)` + ); await sqliteService.executeNonQuery( `CREATE TABLE IF NOT EXISTS memos (user_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)` ); @@ -1104,6 +1107,59 @@ class Database { } return date; } + + async getModeration(input) { + var userId = input.replaceAll("'", ''); + var row = {}; + await sqliteService.execute((dbRow) => { + var block = false; + var mute = false; + if (dbRow[3] === 1) { + block = true; + } + if (dbRow[4] === 1) { + mute = true; + } + row = { + userId: dbRow[0], + updatedAt: dbRow[1], + displayName: dbRow[2], + block, + mute + }; + }, `SELECT * FROM ${Database.userPrefix}_moderation WHERE user_id = '${userId}'`); + return row; + } + + setModeration(entry) { + var block = 0; + var mute = 0; + if (entry.block) { + block = 1; + } + if (entry.mute) { + mute = 1; + } + sqliteService.executeNonQuery( + `INSERT OR REPLACE INTO ${Database.userPrefix}_moderation (user_id, updated_at, display_name, block, mute) VALUES (@user_id, @updated_at, @display_name, @block, @mute)`, + { + '@user_id': entry.userId, + '@updated_at': entry.updatedAt, + '@display_name': entry.displayName, + '@block': block, + '@mute': mute + } + ); + } + + deleteModeration(userId) { + sqliteService.executeNonQuery( + `DELETE FROM ${Database.userPrefix}_moderation WHERE user_id = @user_id`, + { + '@user_id': userId + } + ); + } } var self = new Database(); diff --git a/html/src/service/gamelog.js b/html/src/service/gamelog.js index 916b4e6e..a167f908 100644 --- a/html/src/service/gamelog.js +++ b/html/src/service/gamelog.js @@ -51,6 +51,15 @@ class GameLogService { gameLog.url = args[0]; break; + case 'photon-event': + gameLog.json = args[0]; + break; + + case 'photon-id': + gameLog.displayName = args[0]; + gameLog.photonId = args[1]; + break; + default: break; } diff --git a/html/src/vr.js b/html/src/vr.js index 64fcdcb6..8500445c 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -11,19 +11,10 @@ import locale from 'element-ui/lib/locale/lang/en'; import MarqueeText from 'vue-marquee-text-component'; Vue.component('marquee-text', MarqueeText); -import configRepository from './repository/config.js'; - (async function () { var $app = null; - await CefSharp.BindObjectAsync( - 'AppApi', - 'WebApi', - 'SharedVariable', - 'SQLite' - ); - - await configRepository.init(); + await CefSharp.BindObjectAsync('AppApi'); Noty.overrideDefaults({ animation: { @@ -170,6 +161,17 @@ import configRepository from './repository/config.js'; } }); + var removeFromArray = function (array, item) { + var {length} = array; + for (var i = 0; i < length; ++i) { + if (array[i] === item) { + array.splice(i, 1); + return true; + } + } + return false; + }; + var $app = { data: { // 1 = 대시보드랑 손목에 보이는거 @@ -179,6 +181,7 @@ import configRepository from './repository/config.js'; cpuUsage: 0, config: {}, downloadProgress: 0, + photonLobbyBotSize: 0, nowPlaying: { url: '', name: '', @@ -289,12 +292,18 @@ import configRepository from './repository/config.js'; $app.methods.configUpdate = function (json) { this.config = JSON.parse(json); + this.hudFeed = []; + this.hudTimeout = []; }; $app.methods.updateDownloadProgress = function (progress) { this.downloadProgress = parseInt(progress, 10); }; + $app.methods.updatePhotonLobbyBotSize = function (size) { + this.photonLobbyBotSize = parseInt(size, 10); + }; + $app.methods.nowPlayingUpdate = function (json) { this.nowPlaying = JSON.parse(json); if (this.appType === '2') { @@ -356,7 +365,7 @@ import configRepository from './repository/config.js'; var text = ''; var img = ''; if (image) { - img = ``; + img = ``; } switch (noty.type) { case 'OnPlayerJoined': @@ -455,6 +464,18 @@ import configRepository from './repository/config.js'; case 'MutedOnPlayerLeft': text = `Muted user ${noty.displayName} has left`; break; + case 'Blocked': + text = `${noty.displayName} has blocked you`; + break; + case 'Unblocked': + text = `${noty.displayName} has unblocked you`; + break; + case 'Muted': + text = `${noty.displayName} has muted you`; + break; + case 'Unmuted': + text = `${noty.displayName} has unmuted you`; + break; default: break; } @@ -510,6 +531,61 @@ import configRepository from './repository/config.js'; Noty.closeAll(); }; + $app.data.hudFeed = []; + $app.data.cleanHudFeedLoopStatus = false; + + $app.methods.cleanHudFeedLoop = function () { + if (!this.cleanHudFeedLoopStatus) { + return; + } + this.cleanHudFeed(); + if (this.hudFeed.length === 0) { + this.cleanHudFeedLoopStatus = false; + return; + } + setTimeout(() => this.cleanHudFeedLoop(), 500); + }; + + $app.methods.cleanHudFeed = function () { + var dt = Date.now(); + this.hudFeed.forEach((item) => { + if (item.time + 6000 < dt) { + removeFromArray(this.hudFeed, item); + } + }); + if (this.hudFeed.length > 10) { + this.hudFeed.length = 10; + } + if (!this.cleanHudFeedLoopStatus) { + this.cleanHudFeedLoopStatus = true; + this.cleanHudFeedLoop(); + } + }; + + $app.methods.addEntryHudFeed = function (json) { + var {displayName, text} = JSON.parse(json); + var combo = 1; + this.hudFeed.forEach((item) => { + if (item.displayName === displayName && item.text === text) { + combo = item.combo + 1; + removeFromArray(this.hudFeed, item); + } + }); + this.hudFeed.unshift({ + time: Date.now(), + displayName, + text, + combo + }); + this.cleanHudFeed(); + }; + + $app.data.hudTimeout = []; + + $app.methods.updateHudTimeout = function (json) { + this.hudTimeout = JSON.parse(json); + }; + $app = new Vue($app); window.$app = $app; })(); diff --git a/html/src/vr.pug b/html/src/vr.pug index e16008da..4d5a69a8 100644 --- a/html/src/vr.pug +++ b/html/src/vr.pug @@ -397,6 +397,7 @@ html span(style="float:right") {{ lastLocationTimer }} span(style="display:inline-block") {{ lastLocation.playerList.length }} span(style="display:inline-block;font-weight:bold") {{ lastLocation.friendList.length !== 0 ? `‎‎‎‎‎‎‎‎‏‏‎ ‎(${lastLocation.friendList.length})` : ''}} + span(v-if="photonLobbyBotSize > 0 && lastLocation.playerList.length > 0" style="display:inline-block;color:red;margin-left:5px") {{ photonLobbyBotSize }} template(v-else) template(v-if="downloadProgress === 100") span(style="display:inline-block;margin-right:5px") Downloading: #[i.el-icon-loading] @@ -406,11 +407,24 @@ html span(style="float:right") Timer: {{ lastLocationTimer }} span(style="display:inline-block") Players: {{ lastLocation.playerList.length }} span(style="display:inline-block;font-weight:bold") {{ lastLocation.friendList.length !== 0 ? `‎‎‎‎‎‎‎‎‏‏‎ ‎(${lastLocation.friendList.length})` : ''}} + span(v-if="photonLobbyBotSize > 0 && lastLocation.playerList.length > 0" style="display:inline-block;color:red;margin-left:5px") {{ photonLobbySize }} br span(style="float:right") {{ currentTime | formatDate('YYYY-MM-DD HH:MI:SS AMPM') }} span CPU {{ cpuUsage }}% template(v-else) svg(class="np-progress-circle") circle(class="np-progress-circle-stroke" cx="60" cy="60" stroke="white" r="30" fill="transparent" stroke-width="60") + .hud-feed + div(v-for="feed in hudFeed") + .item {{ feed.displayName }} {{ feed.text }} + template(v-if="feed.combo > 1") + span.combo x{{ feed.combo }} + .hud-timeout(v-if="hudTimeout.length > 0") + .hud-timeout-feed + div(v-for="feed in hudTimeout") + p.item ({{ feed.time }}s) {{ feed.displayName }} + svg(version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve") + path(fill="#ED1B24" d="M68.6,96.5L87,78.1c1.6-1.6,1.6-4.1,0-5.7s-4.1-1.6-5.7,0L62.9,90.9L44.5,72.5l18.4-18.4c1.6-1.6,1.6-4.1,0-5.7c-1.6-1.6-4.1-1.6-5.7,0L38.9,66.8l-6.4-6.4L21.2,71.8C11,82,9.7,97.9,17.4,109.5L0,126.9l8.5,8.5L25.9,118c11.6,7.7,27.5,6.4,37.8-3.8L75,102.9C75,102.9,68.6,96.5,68.6,96.5z") + path(fill="#ED1B24" d="M102.9,75l11.3-11.3c10.3-10.3,11.5-26.1,3.8-37.8l17.4-17.4L126.9,0l-17.4,17.4C97.9,9.7,82,11,71.8,21.2L60.5,32.5C102,74,60.8,32.9,102.9,75z") script(src="vendor.js") script(src="vr.js") diff --git a/html/src/vr.scss b/html/src/vr.scss index f36121d5..1a1d4375 100644 --- a/html/src/vr.scss +++ b/html/src/vr.scss @@ -179,8 +179,8 @@ button { font-family: 'Noto Sans JP', 'Noto Sans KR', 'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif; line-height: normal; - text-shadow: #000 0px 0px 2px, #000 0px 0px 2px, #000 0px 0px 2px, - #000 0px 0px 2px, #000 0px 0px 2px, #000 0px 0px 2px; + text-shadow: #000 0px 0px 3px, #000 0px 0px 3px, #000 0px 0px 3px, + #000 0px 0px 3px, #000 0px 0px 3px, #000 0px 0px 3px; } .x-app { @@ -350,3 +350,49 @@ i.x-user-status.busy { transform: rotate(359deg); } } + +.hud-feed { + position: absolute; + right: 0; + width: 100%; +} + +.hud-feed .item, +.hud-timeout .item { + margin: 0; + text-align: right; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.hud-feed .item { + font-size: 32px; +} + +.hud-feed .combo { + color: #aaa; +} + +.hud-timeout .item { + font-size: 40px; +} + +.hud-timeout { + position: absolute; + bottom: 0; + right: 0; +} + +.hud-timeout-feed { + position: absolute; + bottom: 150px; + right: 0; + color: #ed1b24; +} + +.hud-timeout svg { + position: absolute; + right: -160px; + bottom: 0; +}