// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
// All rights reserved.
//
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using NLog;

#if !LINUX
using CefSharp;
#endif

namespace VRCX
{
    /// <summary>
    /// Monitors the VRChat log files for changes and provides access to the log data.
    /// </summary>
    public class LogWatcher
    {
        public static readonly LogWatcher Instance;
        private static readonly Logger logger = LogManager.GetCurrentClassLogger();
        private Dictionary<string, LogContext> m_LogContextMap; // <FileName, LogContext>
        private DirectoryInfo m_LogDirectoryInfo;
        private List<string[]> m_LogList;
        private ReaderWriterLockSlim m_LogListLock;
        private bool m_FirstRun = true;
        private bool m_ResetLog;
        private Thread m_Thread;
        private DateTime tillDate = DateTime.UtcNow;
        public bool VrcClosedGracefully;
        private readonly ConcurrentQueue<string> m_LogQueue = new ConcurrentQueue<string>(); // for electron

        // NOTE
        // FileSystemWatcher() is unreliable

        static LogWatcher()
        {
            Instance = new LogWatcher();
        }

        public void Init()
        {
            var logPath = Program.AppApiInstance.GetVRChatAppDataLocation();
            m_LogDirectoryInfo = new DirectoryInfo(logPath);
            m_LogContextMap = new Dictionary<string, LogContext>();
            m_LogListLock = new ReaderWriterLockSlim();
            m_LogList = new List<string[]>();
            m_Thread = new Thread(ThreadLoop)
            {
                IsBackground = true
            };
            m_Thread.Start();
        }

        public void Exit()
        {
            var thread = m_Thread;
            m_Thread = null;
            thread.Interrupt();
            thread.Join();
        }

        public void Reset()
        {
            m_ResetLog = true;
            m_Thread?.Interrupt();
        }

        public void SetDateTill(string date)
        {
            tillDate = DateTime.Parse(date, CultureInfo.InvariantCulture, DateTimeStyles.None).ToUniversalTime();
            logger.Info("SetDateTill: {0}", tillDate.ToLocalTime());
        }

        private void ThreadLoop()
        {
            while (m_Thread != null)
            {
                Update();

                try
                {
                    Thread.Sleep(1000);
                }
                catch (ThreadInterruptedException)
                {
                }
            }
        }

        /// <summary>
        /// Updates the log watcher by checking for new log files and updating the log list.
        /// </summary>
        private void Update()
        {
            if (m_ResetLog)
            {
                m_FirstRun = true;
                m_ResetLog = false;
                m_LogContextMap.Clear();
                m_LogListLock.EnterWriteLock();
                try
                {
                    m_LogList.Clear();
                }
                finally
                {
                    m_LogListLock.ExitWriteLock();
                }
            }

            var deletedNameSet = new HashSet<string>(m_LogContextMap.Keys);
            m_LogDirectoryInfo.Refresh();

            if ((m_LogDirectoryInfo.LinkTarget == null && m_LogDirectoryInfo.Exists) || Directory.Exists(m_LogDirectoryInfo.LinkTarget))
            {
                var fileInfos = m_LogDirectoryInfo.GetFiles("output_log_*.txt", SearchOption.TopDirectoryOnly);

                // sort by creation time
                Array.Sort(fileInfos, (a, b) => a.CreationTimeUtc.CompareTo(b.CreationTimeUtc));

                foreach (var fileInfo in fileInfos)
                {
                    fileInfo.Refresh();
                    if (fileInfo.Exists == false)
                    {
                        continue;
                    }

                    if (DateTime.Compare(fileInfo.LastWriteTimeUtc, tillDate) < 0)
                    {
                        continue;
                    }

                    if (m_LogContextMap.TryGetValue(fileInfo.Name, out var logContext))
                    {
                        deletedNameSet.Remove(fileInfo.Name);
                    }
                    else
                    {
                        logContext = new LogContext();
                        m_LogContextMap.Add(fileInfo.Name, logContext);
                    }

                    if (logContext.Length == fileInfo.Length)
                    {
                        continue;
                    }

                    logContext.Length = fileInfo.Length;
                    ParseLog(fileInfo, logContext);
                }
            }

            foreach (var name in deletedNameSet)
            {
                m_LogContextMap.Remove(name);
            }

            m_FirstRun = false;
        }

        /// <summary>
        /// Parses the log file starting from the current position and updates the log context.
        /// </summary>
        /// <param name="fileInfo">The file information of the log file to parse.</param>
        /// <param name="logContext">The log context to update.</param>
        private void ParseLog(FileInfo fileInfo, LogContext logContext)
        {
            var line = string.Empty;
            try
            {
                using var stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 65536, FileOptions.SequentialScan);
                stream.Position = logContext.Position;
                using var streamReader = new StreamReader(stream, Encoding.UTF8);
                while (true)
                {
                    line = streamReader.ReadLine();
                    if (line == null)
                    {
                        logContext.Position = stream.Position;
                        break;
                    }

                    if (line.Length == 0)
                    {
                        continue;
                    }

                    // 2020.10.31 23:36:28 Log        -  [VRCFlowManagerVRC] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd
                    // 2021.02.03 10:18:58 Log        -  [ǄǄǅǅǅǄǄǅǅǄǅǅǅǅǄǄǄǅǅǄǄǅǅǅǅǄǅǅǅǅǄǄǄǄǄǅǄǅǄǄǄǅǅǄǅǅǅ] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd

                    if (ParseLogUdonException(fileInfo, line))
                        continue;

                    if (line.Length <= 36 ||
                        line[31] != '-')
                    {
                        continue;
                    }

                    if (DateTime.TryParseExact(
                            line.Substring(0, 19),
                            "yyyy.MM.dd HH:mm:ss",
                            CultureInfo.InvariantCulture,
                            DateTimeStyles.None,
                            out var lineDate
                        ))
                    {
                        lineDate = lineDate.ToUniversalTime();
                        // check if date is older than last database entry
                        if (DateTime.Compare(lineDate, tillDate) <= 0)
                        {
                            // logger.Warn("Invalid log time, too old: {0}", line);
                            continue;
                        }
                        // check if datetime is over an hour into the future (compensate for gamelog not handling daylight savings time correctly)
                        if (DateTime.UtcNow.AddMinutes(61) < lineDate)
                        {
                            logger.Warn("Invalid log time, too new: {0}", line);
                            continue;
                        }
                    }
                    else
                    {
                        logger.Warn("Failed to parse log date: {0}", line);
                        continue;
                    }

                    var offset = 34;
                    if (line[offset] == '[')
                    {
                        if (ParseLogOnPlayerJoinedOrLeft(fileInfo, logContext, line, offset) ||
                            ParseLogLocation(fileInfo, logContext, line, offset) ||
                            ParseLogLocationDestination(fileInfo, logContext, line, offset) ||
                            ParseLogPortalSpawn(fileInfo, logContext, line, offset) ||
                            ParseLogNotification(fileInfo, logContext, line, offset) ||
                            ParseLogAPIRequest(fileInfo, logContext, line, offset) ||
                            ParseLogAvatarChange(fileInfo, logContext, line, offset) ||
                            ParseLogJoinBlocked(fileInfo, logContext, line, offset) ||
                            ParseLogAvatarPedestalChange(fileInfo, logContext, line, offset) ||
                            ParseLogVideoError(fileInfo, logContext, line, offset) ||
                            ParseLogVideoChange(fileInfo, logContext, line, offset) ||
                            ParseLogAVProVideoChange(fileInfo, logContext, line, offset) ||
                            ParseLogUsharpVideoPlay(fileInfo, logContext, line, offset) ||
                            ParseLogUsharpVideoSync(fileInfo, logContext, line, offset) ||
                            ParseLogWorldVRCX(fileInfo, logContext, line, offset) ||
                            ParseLogWorldDataVRCX(fileInfo, logContext, line, offset) ||
                            ParseLogOnAudioConfigurationChanged(fileInfo, logContext, line, offset) ||
                            ParseLogScreenshot(fileInfo, logContext, line, offset) ||
                            ParseLogStringDownload(fileInfo, logContext, line, offset) ||
                            ParseLogImageDownload(fileInfo, logContext, line, offset) ||
                            ParseVoteKick(fileInfo, logContext, line, offset) ||
                            ParseFailedToJoin(fileInfo, logContext, line, offset) ||
                            ParseInstanceResetWarning(fileInfo, logContext, line, offset) ||
                            ParseVoteKickInitiation(fileInfo, logContext, line, offset) ||
                            ParseVoteKickSuccess(fileInfo, logContext, line, offset) ||
                            ParseStickerSpawn(fileInfo, logContext, line, offset))
                        {
                        }
                    }
                    else
                    {
                        if (ParseLogShaderKeywordsLimit(fileInfo, logContext, line, offset) ||
                            ParseLogSDK2VideoPlay(fileInfo, logContext, line, offset) ||
                            ParseApplicationQuit(fileInfo, logContext, line, offset) ||
                            ParseOpenVRInit(fileInfo, logContext, line, offset) ||
                            ParseDesktopMode(fileInfo, logContext, line, offset) ||
                            ParseOscFailedToStart(fileInfo, logContext, line, offset))
                        {
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                logger.Warn(ex, "Failed to parse log file: {0} {1} {2}", fileInfo.FullName, line, ex.Message);
            }
        }

        private void AppendLog(string[] item)
        {
            m_LogListLock.EnterWriteLock();
            try
            {
                if (!m_FirstRun)
                {
                    var logLine = JsonSerializer.Serialize(item);
#if LINUX
                    m_LogQueue.Enqueue(logLine);
#else
                    if (MainForm.Instance != null && MainForm.Instance.Browser != null)
                        MainForm.Instance.Browser.ExecuteScriptAsync("$app.addGameLogEvent", logLine);
#endif
                }

                m_LogList.Add(item);
            }
            finally
            {
                m_LogListLock.ExitWriteLock();
            }
        }

        public List<string> GetLogLines()
        {
            // for electron
            var logLines = new List<string>();
            while (m_LogQueue.TryDequeue(out var logLine))
                logLines.Add(logLine);

            return logLines;
        }

        private string ConvertLogTimeToISO8601(string line)
        {
            // 2020.10.31 23:36:22

            if (DateTime.TryParseExact(
                    line.Substring(0, 19),
                    "yyyy.MM.dd HH:mm:ss",
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.None,
                    out var dt
                ))
            {
                dt = dt.ToUniversalTime();
            }
            else
            {
                dt = DateTime.UtcNow;
            }

            // ISO 8601
            return dt.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'", CultureInfo.InvariantCulture);
        }

        private bool ParseLogLocation(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2020.10.31 23:36:28 Log        -  [VRCFlowManagerVRC] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd
            // 2020.10.31 23:36:28 Log        -  [VRCFlowManagerVRC] Destination set: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd
            // 2020.10.31 23:36:31 Log        -  [RoomManager] Entering Room: VRChat Home
            // 2020.10.31 23:36:31 Log        -  [RoomManager] Joining wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd:67646~private(usr_4f76a584-9d4b-46f6-8209-8305eb683661)~nonce(D9298A536FEEEDDBB61633661A4BDAA09717C5178DEF865C4C09372FE12E09A6)
            // 2020.10.31 23:36:31 Log        -  [RoomManager] Joining or Creating Room: VRChat Home
            // 2020.10.31 23:36:31 Log        -  [RoomManager] Successfully joined room
            // 2021.02.03 10:18:58 Log        -  [ǄǄǅǅǅǄǄǅǅǄǅǅǅǅǄǄǄǅǅǄǄǅǅǅǅǄǅǅǅǅǄǄǄǄǄǅǄǅǄǄǄǅǅǄǅǅǅ] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd
            // 2021.06.23 12:02:56 Log        -  [Behaviour] Entering Room: VRChat Home

            if (line.Contains("[Behaviour] Entering Room: "))
            {
                var lineOffset = line.LastIndexOf("] Entering Room: ", StringComparison.Ordinal);
                if (lineOffset < 0)
                    return true;
                lineOffset += 17;
                if (lineOffset >= line.Length)
                    return true;

                var worldName = line.Substring(lineOffset);
                logContext.RecentWorldName = worldName;
                return true;
            }

            if (line.Contains("[Behaviour] Joining ") && !line.Contains("] Joining or Creating Room: ") && !line.Contains("] Joining friend: "))
            {
                var lineOffset = line.LastIndexOf("] Joining ", StringComparison.Ordinal);
                if (lineOffset < 0)
                    return true;
                lineOffset += 10;
                if (lineOffset >= line.Length)
                    return true;

                var location = line.Substring(lineOffset);

                AppendLog(new[]
                {
                    fileInfo.Name,
                    ConvertLogTimeToISO8601(line),
                    "location",
                    location,
                    logContext.RecentWorldName
                });

                // logContext.onJoinPhotonDisplayName = string.Empty;
                // logContext.onJoinPhotonDisplayNameDate = string.Empty;
                logContext.LastAudioDevice = string.Empty;
                logContext.LastVideoError = string.Empty;
                logContext.locationDestination = string.Empty;
                VrcClosedGracefully = false;

                return true;
            }

            return false;
        }

        private bool ParseLogScreenshot(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // This won't work with VRChat's new "Multi Layer" camera mode, since it doesn't output any logs like normal pictures.
            // 2023.02.08 12:31:35 Log        -  [VRC Camera] Took screenshot to: C:\Users\Tea\Pictures\VRChat\2023-02\VRChat_2023-02-08_12-31-35.104_1920x1080.png
            if (!line.Contains("[VRC Camera] Took screenshot to: "))
                return false;

            var lineOffset = line.LastIndexOf("] Took screenshot to: ", StringComparison.Ordinal);
            if (lineOffset < 0)
                return true;

            var screenshotPath = line.Substring(lineOffset + 22);
            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "screenshot",
                screenshotPath
            });
            return true;
        }

        private bool ParseLogLocationDestination(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 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
            // 2022.08.13 18:57:00 Log        -  [Behaviour] OnLeftRoom
            // 2024.11.22 15:32:28 Log        -  [Behaviour] Successfully left room

            if (line.Contains("[Behaviour] Successfully left room"))
            {
                AppendLog(new[]
                {
                    fileInfo.Name,
                    ConvertLogTimeToISO8601(line),
                    "location-destination",
                    logContext.locationDestination
                });

                logContext.locationDestination = string.Empty;

                return true;
            }

            if (line.Contains("[Behaviour] Destination fetching: "))
            {
                var lineOffset = line.LastIndexOf("] Destination fetching: ", StringComparison.Ordinal);
                if (lineOffset < 0)
                    return true;
                lineOffset += 24;
                if (lineOffset >= line.Length)
                    return true;

                logContext.locationDestination = line.Substring(lineOffset);

                return true;
            }

            return false;
        }

        private bool ParseLogOnPlayerJoinedOrLeft(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2020.10.31 23:36:58 Log        -  [NetworkManager] OnPlayerJoined pypy
            // 2020.10.31 23:36:58 Log        -  [Player] Initialized PlayerAPI "pypy" is local
            // 2020.10.31 23:36:58 Log        -  [NetworkManager] OnPlayerJoined Rize♡
            // 2020.10.31 23:36:58 Log        -  [Player] Initialized PlayerAPI "Rize♡" is remote

            // 2020.11.01 00:07:01 Log        -  [NetworkManager] OnPlayerLeft Rize♡
            // 2020.11.01 00:07:01 Log        -  [PlayerManager] Removed player 2 / Rize♡
            // 2020.11.01 00:07:02 Log        -  [Player] Unregistering Rize♡

            // 2021.06.23 11:41:16 Log        -  [Behaviour] Initialized PlayerAPI "Natsumi-sama" is local

            // 2021.12.12 11:47:22 Log        -  [Behaviour] OnPlayerJoined Natsumi-sama
            // 2021.12.12 11:47:22 Log        -  [Behaviour] OnPlayerJoined:Unnamed
            // 2021.12.12 11:53:14 Log        -  [Behaviour] OnPlayerLeftRoom

            // Future logs will be formatted like this: [Behaviour] OnPlayerJoined Natsumi-sama (usr_032383a7-748c-4fb2-94e4-bcb928e5de6b)

            if (line.Contains("[Behaviour] OnPlayerJoined") && !line.Contains("] OnPlayerJoined:"))
            {
                var lineOffset = line.LastIndexOf("] OnPlayerJoined", StringComparison.Ordinal);
                if (lineOffset < 0)
                    return true;
                lineOffset += 17;
                if (lineOffset > line.Length)
                    return true;

                var userInfo = ParseUserInfo(line.Substring(lineOffset));

                AppendLog(new[]
                {
                    fileInfo.Name,
                    ConvertLogTimeToISO8601(line),
                    "player-joined",
                    userInfo.DisplayName,
                    userInfo.UserId
                });

                return true;
            }

            if (line.Contains("[Behaviour] OnPlayerLeft") && !line.Contains("] OnPlayerLeftRoom") && !line.Contains("] OnPlayerLeft:"))
            {
                var lineOffset = line.LastIndexOf("] OnPlayerLeft", StringComparison.Ordinal);
                if (lineOffset < 0)
                    return true;
                lineOffset += 15;
                if (lineOffset > line.Length)
                    return true;

                var userInfo = ParseUserInfo(line.Substring(lineOffset));

                AppendLog(new[]
                {
                    fileInfo.Name,
                    ConvertLogTimeToISO8601(line),
                    "player-left",
                    userInfo.DisplayName,
                    userInfo.UserId
                });

                return true;
            }

            return false;
        }

        private bool ParseLogPortalSpawn(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 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)
            // 2022.07.29 18:40:37 Log        -  [Behaviour] Instantiated a (Clone [800004] Portals/PortalInternalDynamic)
            // 2023 - deadge

            if (line.Contains("[Behaviour] Instantiated a (Clone [") && line.Contains("] Portals/PortalInternalDynamic)"))
            {
                AppendLog(new[]
                {
                    fileInfo.Name,
                    ConvertLogTimeToISO8601(line),
                    "portal-spawn"
                });
                return true;
            }

            return false;
        }

        private bool ParseLogShaderKeywordsLimit(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 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 (line.Contains("Maximum number (384) of shader global keywords exceeded"))
            {
                if (logContext.ShaderKeywordsLimitReached)
                    return true;

                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 (!line.Contains("] Master is not sending any events! Moving to a new instance."))
                return false;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "event",
                "Joining instance blocked by master"
            });

            return true;
        }

        private bool ParseLogAvatarPedestalChange(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 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);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "event",
                $"{data} changed avatar pedestal"
            });

            return true;
        }

        private bool ParseLogVideoError(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2021.04.08 06:37:45 Error -  [Video Playback] ERROR: Video unavailable
            // 2021.04.08 06:40:07 Error -  [Video Playback] ERROR: Private video

            // 2024.07.31 22:28:47 Error      -  [AVProVideo] Error: Loading failed.  File not found, codec not supported, video resolution too high or insufficient system resources.
            // 2024.07.31 23:04:15 Error      -  [AVProVideo] Error: Loading failed.  File not found, codec not supported, video resolution too high or insufficient system resources.

            if (line.Contains("[Video Playback] ERROR: "))
            {
                var data = line.Substring(offset + 24);
                if (data == logContext.LastVideoError)
                    return true;
                logContext.LastVideoError = data;

                AppendLog(new[]
                {
                    fileInfo.Name,
                    ConvertLogTimeToISO8601(line),
                    "event",
                    "VideoError: " + data
                });

                return true;
            }

            if (line.Contains("[AVProVideo] Error: "))
            {
                var data = line.Substring(offset + 20);
                if (data == logContext.LastVideoError)
                    return true;
                logContext.LastVideoError = data;

                AppendLog(new[]
                {
                    fileInfo.Name,
                    ConvertLogTimeToISO8601(line),
                    "event",
                    "VideoError: " + data
                });

                return true;
            }

            return false;
        }

        private bool ParseLogWorldVRCX(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // [VRCX] VideoPlay(PyPyDance) "https://jd.pypy.moe/api/v1/videos/-Q3pdlsQxOk.mp4",0.5338666,260.6938,"1339 : Le Freak (Random)"

            if (string.Compare(line, offset, "[VRCX] ", 0, 7, StringComparison.Ordinal) != 0)
                return false;

            var data = line.Substring(offset + 7);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "vrcx",
                data
            });

            return true;
        }

        private bool ParseLogWorldDataVRCX(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // [VRCX-World] store:test:testvalue

            if (string.Compare(line, offset, "[VRCX-World] ", 0, 13, StringComparison.Ordinal) != 0)
                return false;

            var data = line.Substring(offset + 13);

#if !LINUX
            WorldDBManager.Instance.ProcessLogWorldDataRequest(data);
#endif
            return true;
        }

        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);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "video-play",
                data
            });

            return true;
        }

        private bool ParseLogAVProVideoChange(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2023.05.12 15:53:48 Log        -  [Video Playback] Resolving URL 'rtspt://topaz.chat/live/kiriri520'

            if (string.Compare(line, offset, "[Video Playback] Resolving URL '", 0, 32, StringComparison.Ordinal) != 0)
                return false;

            var pos = line.LastIndexOf('\'');
            if (pos < 0)
                return false;

            var data = line.Substring(offset + 32);
            data = data.Remove(data.Length - 1);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "video-play",
                data
            });

            return true;
        }

        private bool ParseLogSDK2VideoPlay(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 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 ", StringComparison.Ordinal);
            if (pos < 0)
                return false;

            var displayName = line.Substring(offset + 5, pos - (offset + 5));
            var data = line.Substring(pos + 11);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "video-play",
                data,
                displayName
            });

            return true;
        }

        private bool ParseLogUsharpVideoPlay(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2021.12.12 05:51:58 Log        -  [USharpVideo] Started video load for URL: https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1s, requested by ʜ ᴀ ᴘ ᴘ ʏ

            if (string.Compare(line, offset, "[USharpVideo] Started video load for URL: ", 0, 42, StringComparison.Ordinal) != 0)
                return false;

            var pos = line.LastIndexOf(", requested by ", StringComparison.Ordinal);
            if (pos < 0)
                return false;

            var data = line.Substring(offset + 42, pos - (offset + 42));
            var displayName = line.Substring(pos + 15);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "video-play",
                data,
                displayName
            });

            return true;
        }

        private bool ParseLogUsharpVideoSync(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2022.01.16 05:20:23 Log        -  [USharpVideo] Syncing video to 2.52

            if (string.Compare(line, offset, "[USharpVideo] Syncing video to ", 0, 31, StringComparison.Ordinal) != 0)
                return false;

            var data = line.Substring(offset + 31);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "video-sync",
                data
            });

            return true;
        }

        private bool ParseLogNotification(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 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 ", StringComparison.Ordinal);
            if (pos < 0)
                return false;

            var data = line.Substring(offset + 30, pos - (offset + 30));

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "notification",
                data
            });

            return true;
        }

        private bool ParseLogAPIRequest(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2021.10.03 09:49:50 Log        -  [API] [110] Sending Get request to https://api.vrchat.cloud/api/1/worlds?apiKey=JlE5Jldo5Jibnk5O5hTx6XVqsJu4WJ26&organization=vrchat&userId=usr_032383a7-748c-4fb2-94e4-bcb928e5de6b&n=99&order=descending&offset=0&releaseStatus=public&maxUnityVersion=2019.4.31f1&minUnityVersion=5.5.0f1&maxAssetVersion=4&minAssetVersion=0&platform=standalonewindows
            // 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 ", StringComparison.Ordinal);
            if (pos < 0)
                return false;

            var data = line.Substring(pos + 25);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "api-request",
                data
            });

            return true;
        }

        private bool ParseLogAvatarChange(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2023.11.05 14:45:57 Log        -  [Behaviour] Switching K․MOG to avatar MoeSera

            if (string.Compare(line, offset, "[Behaviour] Switching ", 0, 22, StringComparison.Ordinal) != 0)
                return false;

            var pos = line.LastIndexOf(" to avatar ", StringComparison.Ordinal);
            if (pos < 0)
                return false;

            var displayName = line.Substring(offset + 22, pos - (offset + 22));
            var avatarName = line.Substring(pos + 11);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "avatar-change",
                displayName,
                avatarName
            });

            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:10:28 Log        -  [Behaviour] Initialize Limb Avatar (UnityEngine.Animator) VRCPlayer[Remote] 78614426 59 (ǄǄǄǅǄǅǅǄǅǄǄǅǅǄǅǄǅǅǅǄǄǄǅǄǄǅǅǄǅǅǄǅǅǄǅǅǅǅǄǅǄǅǄǄǄǄǅ) False Loading
        //     // 2021.11.10 08:57:32 Log        -  [Behaviour] Initialize Limb Avatar (UnityEngine.Animator) VRCPlayer[Local] 59136629 1 (ǄǄǄǅǄǅǅǄǅǄǄǅǅǄǅǄǅǅǅǄǄǄǅǄǄǅǅǄǅǅǄǅǅǄǅǅǅǅǄǅǄǅǄǄǄǄǅ) True Loading
        //
        //     // 2022.03.05 11:29:16 Log        -  [Behaviour] Initialize ThreePoint Avatar (UnityEngine.Animator) VRCPlayer[Local] 50608765 1 (ǄǅǄǄǄǅǄǅǅǄǅǄǄǅǅǄǄǄǅǄǄǄǅǄǅǄǅǅǄǄǄǄǅǅǄǄǄǄǅǅǄǄǅǄǄǅǅ) True Custom
        //
        //     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("[Behaviour] Initialized player "))
        //     {
        //         var pos = line.LastIndexOf("[Behaviour] Initialized player ");
        //         if (pos < 0)
        //             return false;
        //
        //         pos += 31;
        //         if (pos >= line.Length)
        //             return false;
        //         
        //         var displayName = line.Substring(pos, line.Length - pos);
        //         logContext.onJoinPhotonDisplayName = displayName;
        //         logContext.onJoinPhotonDisplayNameDate = ConvertLogTimeToISO8601(line);
        //
        //         return true;
        //     }
        //
        //     if (line.Contains("[Behaviour] Configuring remote player VRCPlayer[Remote] "))
        //     {
        //         if (string.IsNullOrEmpty(logContext.onJoinPhotonDisplayName) || 
        //             logContext.onJoinPhotonDisplayNameDate != ConvertLogTimeToISO8601(line))
        //             return false;
        //
        //         var pos = line.LastIndexOf("[Behaviour] Configuring remote player VRCPlayer[Remote] ");
        //         if (pos < 0)
        //             return false;
        //
        //         pos += 56;
        //         var startPos = pos + 9;
        //         var length = line.Length - startPos;
        //         var photonId = line.Substring(startPos, length);
        //
        //         AppendLog(new[]
        //         {
        //             fileInfo.Name,
        //             ConvertLogTimeToISO8601(line),
        //             "photon-id",
        //             logContext.onJoinPhotonDisplayName,
        //             photonId
        //         });
        //         logContext.onJoinPhotonDisplayName = string.Empty;
        //         logContext.onJoinPhotonDisplayNameDate = string.Empty;
        //
        //         return true;
        //     }
        //
        //     return false;
        // }

        private bool ParseLogOnAudioConfigurationChanged(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2022.03.15 03:40:34 Log        -  [Always] uSpeak: SetInputDevice 0 (3 total) 'Index HMD Mic (Valve VR Radio & HMD Mic)'
            // 2022.03.15 04:02:22 Log        -  [Always] uSpeak: OnAudioConfigurationChanged - devicesChanged = True, resetting mic..
            // 2022.03.15 04:02:22 Log        -  [Always] uSpeak: SetInputDevice by name 'Index HMD Mic (Valve VR Radio & HMD Mic)' (3 total)
            // 2025.01.03 19:11:42 Log        -  [Always] uSpeak: SetInputDevice 0 (2 total) 'Microphone (NVIDIA Broadcast)'

            if (line.Contains("[Always] uSpeak: OnAudioConfigurationChanged"))
            {
                logContext.AudioDeviceChanged = true;
                return true;
            }

            if (line.Contains("[Always] uSpeak: SetInputDevice 0"))
            {
                var lineOffset = line.LastIndexOf(") '", StringComparison.Ordinal);
                if (lineOffset < 0)
                    return true;
                lineOffset += 3;
                var endPos = line.Length - 1;
                var length = Math.Min(endPos - lineOffset + 1, line.Length - lineOffset);
                if (length <= 0)
                    return true;

                var audioDevice = line.Substring(lineOffset, length);
                if (string.IsNullOrEmpty(logContext.LastAudioDevice))
                {
                    logContext.AudioDeviceChanged = false;
                    logContext.LastAudioDevice = audioDevice;
                    return true;
                }

                if (!logContext.AudioDeviceChanged || logContext.LastAudioDevice == audioDevice)
                    return true;

                AppendLog(new[]
                {
                    fileInfo.Name,
                    ConvertLogTimeToISO8601(line),
                    "event",
                    $"Audio device changed, mic set to '{audioDevice}'"
                });

                logContext.LastAudioDevice = audioDevice;
                logContext.AudioDeviceChanged = false;

                return true;
            }

            return false;
        }

        private bool ParseLogUdonException(FileInfo fileInfo, string line)
        {
            // 2022.11.29 04:27:33 Error      -  [UdonBehaviour] An exception occurred during Udon execution, this UdonBehaviour will be halted.
            // VRC.Udon.VM.UdonVMException: An exception occurred in an UdonVM, execution will be halted. --->VRC.Udon.VM.UdonVMException: An exception occurred during EXTERN to 'VRCSDKBaseVRCPlayerApi.__get_displayName__SystemString'. --->System.NullReferenceException: Object reference not set to an instance of an object.

            if (line.Contains("[PyPyDance]"))
            {
                AppendLog(new[]
                {
                    fileInfo.Name,
                    ConvertLogTimeToISO8601(line),
                    "udon-exception",
                    line
                });
                return true;
            }

            var lineOffset = line.IndexOf(" ---> VRC.Udon.VM.UdonVMException: ", StringComparison.Ordinal);
            if (lineOffset < 0)
                return false;

            var data = line.Substring(lineOffset);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "udon-exception",
                data
            });

            return true;
        }

        private bool ParseApplicationQuit(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2022.06.12 01:51:46 Log        -  VRCApplication: OnApplicationQuit at 1603.499
            // 2024.10.23 21:18:34 Log        -  VRCApplication: HandleApplicationQuit at 936.5161
            if (string.Compare(line, offset, "VRCApplication: OnApplicationQuit at ", 0, 37, StringComparison.Ordinal) != 0 &&
                string.Compare(line, offset, "VRCApplication: HandleApplicationQuit at ", 0, 41, StringComparison.Ordinal) != 0)
                return false;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "vrc-quit"
            });

            VrcClosedGracefully = true;

            return true;
        }

        private bool ParseOpenVRInit(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2022.07.29 02:52:14 Log        -  OpenVR initialized!

            // 2023.04.22 16:52:28 Log        -  Initializing VRSDK.
            // 2023.04.22 16:52:29 Log        -  StartVRSDK: Open VR Loader

            // 2024.07.26 01:48:56 Log        -  STEAMVR HMD Model: Index

            if (string.Compare(line, offset, "Initializing VRSDK.", 0, 19, StringComparison.Ordinal) != 0 &&
                string.Compare(line, offset, "STEAMVR HMD Model: ", 0, 20, StringComparison.Ordinal) != 0)
                return false;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "openvr-init"
            });

            return true;
        }

        private bool ParseDesktopMode(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2023.04.22 16:54:18 Log        -  VR Disabled

            if (string.Compare(line, offset, "VR Disabled", 0, 11, StringComparison.Ordinal) != 0)
                return false;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "desktop-mode"
            });

            return true;
        }

        private bool ParseLogStringDownload(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2023.03.23 11:37:21 Log        -  [String Download] Attempting to load String from URL 'https://pastebin.com/raw/BaW6NL2L'
            var check = "] Attempting to load String from URL '";
            if (!line.Contains(check))
                return false;

            var lineOffset = line.LastIndexOf(check, StringComparison.Ordinal);
            if (lineOffset < 0)
                return true;

            var stringData = line.Substring(lineOffset + check.Length);
            stringData = stringData.Remove(stringData.Length - 1);

            if (stringData.StartsWith("http://127.0.0.1:22500") || stringData.StartsWith("http://localhost:22500"))
                return true; // ignore own requests

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "resource-load-string",
                stringData
            });
            return true;
        }

        private bool ParseLogImageDownload(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2023.03.23 11:32:25 Log        -  [Image Download] Attempting to load image from URL 'https://i.imgur.com/lCfUMX0.jpeg'
            var check = "] Attempting to load image from URL '";
            if (!line.Contains(check))
                return false;

            var lineOffset = line.LastIndexOf(check, StringComparison.Ordinal);
            if (lineOffset < 0)
                return true;

            var imageData = line.Substring(lineOffset + check.Length);
            imageData = imageData.Remove(imageData.Length - 1);

            if (imageData.StartsWith("http://127.0.0.1:22500") || imageData.StartsWith("http://localhost:22500"))
                return true; // ignore own requests

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "resource-load-image",
                imageData
            });
            return true;
        }

        private bool ParseVoteKick(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2023.06.02 01:08:04 Log        -  [Behaviour] Received executive message: You have been kicked from the instance by majority vote
            // 2023.06.02 01:11:58 Log        -  [Behaviour] You have been kicked from this world for an hour.

            if (string.Compare(line, offset, "[Behaviour] Received executive message: ", 0, 40, StringComparison.Ordinal) != 0)
                return false;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "event",
                line.Substring(offset + 40)
            });
            return true;
        }

        private bool ParseFailedToJoin(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2023.09.01 10:42:19 Warning    -  [Behaviour] Failed to join instance 'wrld_78eb6b52-fd5a-4954-ba28-972c92c8cc77:82384~hidden(usr_a9bf892d-b447-47ce-a572-20c83dbfffd8)~region(eu)' due to 'That instance is using an outdated version of VRChat. You won't be able to join them until they update!'

            if (string.Compare(line, offset, "[Behaviour] Failed to join instance ", 0, 36, StringComparison.Ordinal) != 0)
                return false;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "event",
                line.Substring(offset + 12)
            });
            return true;
        }

        private bool ParseOscFailedToStart(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2023.09.26 04:12:57 Warning    -  Could not Start OSC: Address already in use

            if (string.Compare(line, offset, "Could not Start OSC: ", 0, 21, StringComparison.Ordinal) != 0)
                return false;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "event",
                $"VRChat couldn't start OSC server, \"{line.Substring(offset)}\""
            });
            return true;
        }

        private bool ParseInstanceResetWarning(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2024.08.30 01:43:40 Log        -  [ModerationManager] This instance will be reset in 60 minutes due to its age.
            if (!line.Contains("[ModerationManager] This instance will be reset in "))
                return false;

            int index = line.IndexOf("[ModerationManager] This instance will be reset in ", StringComparison.Ordinal) + 20;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "event",
                line[index..]
            });

            return true;
        }

        private bool ParseVoteKickInitiation(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2024.08.29 02:04:47 Log        -  [ModerationManager] A vote kick has been initiated against בורקס במאווררים 849d, do you agree?
            if (!line.Contains("[ModerationManager] A vote kick has been initiated against ", StringComparison.Ordinal))
                return false;

            int index = line.IndexOf("[ModerationManager] A vote kick has been initiated against ", StringComparison.Ordinal) + 20;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "event",
                line[index..]
            });

            return true;
        }

        private bool ParseVoteKickSuccess(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            // 2024.08.29 02:05:21 Log        -  [ModerationManager] Vote to kick בורקס במאווררים 849d succeeded
            if (!line.Contains("[ModerationManager] Vote to kick ", StringComparison.Ordinal))
                return false;

            int index = line.IndexOf("[ModerationManager] Vote to kick ", StringComparison.Ordinal) + 20;

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "event",
                line[index..]
            });

            return true;
        }

        private bool ParseStickerSpawn(FileInfo fileInfo, LogContext logContext, string line, int offset)
        {
            var index = line.IndexOf("[StickersManager] User ", StringComparison.Ordinal);
            if (index == -1 || !line.Contains("file_") || !line.Contains("spawned sticker"))
                return false;

            string info = line.Substring(index + 23);

            var (userId, displayName) = ParseUserInfo(info);

            var fileIdIndex = info.IndexOf("file_", StringComparison.Ordinal);
            string fileId = info.Substring(fileIdIndex);

            AppendLog(new[]
            {
                fileInfo.Name,
                ConvertLogTimeToISO8601(line),
                "sticker-spawn",
                userId,
                displayName,
                fileId,
            });

            return true;
        }

        public string[][] Get()
        {
            Update();

            if (m_ResetLog == false &&
                m_LogList.Count > 0)
            {
                m_LogListLock.EnterWriteLock();
                try
                {
                    string[][] items;

                    if (m_LogList.Count > 1000)
                    {
                        items = new string[1000][];
                        m_LogList.CopyTo(0, items, 0, 1000);
                        m_LogList.RemoveRange(0, 1000);
                    }
                    else
                    {
                        items = m_LogList.ToArray();
                        m_LogList.Clear();
                    }

                    return items;
                }
                finally
                {
                    m_LogListLock.ExitWriteLock();
                }
            }

            return new string[][] { };
        }

        private static (string DisplayName, string UserId) ParseUserInfo(string userInfo)
        {
            string userDisplayName;
            string userId;

            int pos = userInfo.LastIndexOf(" (", StringComparison.Ordinal);
            if (pos >= 0)
            {
                userDisplayName = userInfo.Substring(0, pos);
                userId = userInfo.Substring(pos + 2, userInfo.LastIndexOf(')') - (pos + 2));
            }
            else
            {
                userDisplayName = userInfo;
                userId = null;
            }

            return (userDisplayName, userId);
        }

        private class LogContext
        {
            public bool AudioDeviceChanged;
            public string LastAudioDevice;
            public string LastVideoError;
            public long Length;
            public string locationDestination;
            public long Position;
            public string RecentWorldName;
            public bool ShaderKeywordsLimitReached;
        }
    }
}