RPA_Parser.cs:
```
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Ionic.Zlib;
using Microsoft.Win32;
using Razorvine.Pickle;
using WebPWrapper;

namespace RPA_Parser
{
    // Inspired by: https://github.com/Shizmob/rpatool
    // Inspired by: https://github.com/CensoredUsername/unrpyc
    
    public class RpaParser
    {
        public class Version
        {
            public const double Unknown = -1;
            public const double RPA_1 = 1;
            public const double RPA_2 = 2;
            public const double RPA_3 = 3;
            public const double RPA_3_2 = 3.2;
        }
        
        private class ArchiveMagic
        {
            public const string RPA_1_RPA = ".rpa";
            public const string RPA_1_RPI = ".rpi";
            public const string RPA_2 = "RPA-2.0 ";
            public const string RPA_3 = "RPA-3.0 ";
            public const string RPA_3_2 = "RPA-3.2 ";
        }
        
        private class RPCMagic
        {
            public const string RPC_2 = "RENPY RPC2";
        }

        public FileInfo ArchiveInfo;
        public FileInfo IndexInfo;
        public double ArchiveVersion = Version.Unknown;
        public int Padding = 0;
        public long ObfuscationKey = 0xDEADBEEF;
        public bool OptionsConfirmed = false;
        public SortedDictionary<string,ArchiveIndex> Index = new ();

        public string PythonLocation = GetPythonPath("2.7", "2.7");
        public string UnrpycLocation = String.Empty;
        
        private long _offset;
        private string _archivePath;
        private string _indexPath;
        private string _firstLine;
        private string[] _metadata;

        public class Tuples
        {
            public long Offset;
            public long Length;
            public byte[] Prefix;
        }
        
        public class ArchiveIndex
        {
            public readonly SortedDictionary<int, Tuples> Tuples = new ();
            public string FullPath = String.Empty;
            public string TreePath = String.Empty;
            public string ParentPath = String.Empty;
            public bool InArchive;
            public long Length;
        }

        public class PreviewTypes
        {
            public const string Unknown = "unknown";
            public const string Image = "image";
            public const string Text = "text";
            public const string Video = "video";
            public const string Audio = "audio";
        }

        /*
        RenPy Supports:
        Images: JPEG/JPG, PNG, WEBP, BMP, GIF
        Sound/Music: OPUS, OGG Vorbis, FLAC, WAV, MP3, MP2
        Movies: WEBM, OGG Theora, VP9, VP8, MPEG 41, MPEG 2, MPEG 1
        */

        public readonly string[] ImageExtList = {
            ".jpeg",
            ".jpg",
            ".bmp",
            ".tiff",
            ".png",
            ".webp",
            ".exif",
            ".ico",
            ".gif",
            ".avif" // Add this line
        };

        public readonly string[] AudioExtList = {
            ".aac",
            ".ac3",
            ".flac",
            ".mp3",
            ".wma",
            ".wav",
            ".ogg",
            ".cpc"
        };

        public readonly string[] VideoExtList = {
            ".3gp",
            ".flv",
            ".mov",
            ".mp4",
            ".ogv",
            ".swf",
            ".mpg",
            ".mpeg",
            ".avi",
            ".mkv",
            ".wmv",
            ".webm"
        };

        public readonly string[] TextExtList = {
            ".py",
            ".rpy~",
            ".rpy",
            ".txt",
            ".log",
            ".nfo",
            ".htm",
            ".html",
            ".xml",
            ".json",
            ".yaml",
            ".csv"
        };

        public readonly string[] CodeExtList = {
            ".rpyc~",
            ".rpyc",
            ".rpymc~",
            ".rpymc"
        };
        
        // Original: https://stackoverflow.com/a/60757602/3650856
        private static string GetPythonPath(string requiredVersion = "", string maxVersion = "") {
            string[] possiblePythonLocations = new string[3] {
                @"HKLM\SOFTWARE\Python\PythonCore\",
                @"HKCU\SOFTWARE\Python\PythonCore\",
                @"HKLM\SOFTWARE\Wow6432Node\Python\PythonCore\"
            };

            //Version number, install path
            Dictionary<string, string> pythonLocations = new Dictionary<string, string>();

            foreach (string possibleLocation in possiblePythonLocations) {
                string regKey = possibleLocation.Substring(0, 4), actualPath = possibleLocation.Substring(5);
                RegistryKey theKey = (regKey == "HKLM" ? Registry.LocalMachine : Registry.CurrentUser);
                RegistryKey theValue = theKey.OpenSubKey(actualPath);

                if (theValue != null)
                {
                    foreach (string value in theValue.GetSubKeyNames())
                    {
                        RegistryKey productKey = theValue.OpenSubKey(value);
                        if (productKey != null)
                        {
                            try
                            {
                                string pythonExePath = productKey.OpenSubKey("InstallPath")
                                    ?.GetValue("ExecutablePath")
                                    ?.ToString();

                                // Get (Default) value instead if not found in ExecutablePath value
                                if (string.IsNullOrEmpty(pythonExePath))
                                {
                                    pythonExePath = productKey.OpenSubKey("InstallPath")
                                        ?.GetValue("")
                                        .ToString();
                                    if (!string.IsNullOrEmpty(pythonExePath))
                                    {
                                        pythonExePath += "python.exe";
                                    }
                                }

                                if (!string.IsNullOrEmpty(pythonExePath))
                                {
                                    if (!pythonLocations.ContainsKey(value) && File.Exists(pythonExePath))
                                    {
                                        Debug.WriteLine("Got python version; " + value + " at path; " + pythonExePath);
                                        pythonLocations.Add(value, pythonExePath);
                                    }
                                }
                            }
                            catch
                            {
                                // Object doesn't exist
                            }
                        }
                    }
                }
            }

            if (pythonLocations.Count > 0)
            {
                System.Version desiredVersion = new System.Version(requiredVersion == "" ? "0.0.1" : requiredVersion);
                System.Version maxPVersion = new System.Version(maxVersion == "" ? "999.999.999" : maxVersion);

                string highestVersion = "";
                string highestVersionPath = "";

                foreach (KeyValuePair<string, string> pVersion in pythonLocations) {
                    // TODO; if on 64-bit machine, prefer the 64 bit version over 32 and vice versa
                    int index = pVersion.Key.IndexOf("-", StringComparison.Ordinal);
                    string formattedVersion = index > 0 ? pVersion.Key.Substring(0, index) : pVersion.Key;

                    System.Version thisVersion = new System.Version(formattedVersion);
                    int comparison = desiredVersion.CompareTo(thisVersion);
                    int maxComparison = maxPVersion.CompareTo(thisVersion);

                    if (comparison <= 0) {
                        // Version is greater or equal
                        if (maxComparison >= 0) {
                            desiredVersion = thisVersion;

                            highestVersion = pVersion.Key;
                            highestVersionPath = pVersion.Value;
                        }
                    }
                }

                Debug.WriteLine(highestVersion);
                Debug.WriteLine(highestVersionPath);
                return highestVersionPath;
            }

            return String.Empty;
        }

        public void LoadArchive(string filePath)
        {
            _archivePath = filePath;
            GetIndexAndArchive();
            ArchiveInfo = GetArchiveInfo();
            _firstLine = GetFirstLine();
            ArchiveVersion = CheckSupportedVersion(GetVersion());
            
            if (CheckVersion(ArchiveVersion, Version.RPA_2) || CheckVersion(ArchiveVersion, Version.RPA_3) || CheckVersion(ArchiveVersion, Version.RPA_3_2))
            {
                _metadata = GetMetadata();
                _offset = GetOffset();
                ObfuscationKey = GetObfuscationKey();
            }
            else if (CheckVersion(ArchiveVersion, Version.RPA_1))
            {
                IndexInfo = GetIndexInfo();
            }

            Index = GetIndexes();
        }

        public bool CheckVersion(double version, double check)
        {
            double difference = version - check;
            if (difference == 0)
            {
                return true;
            }

            return false;
        }

        private void GetIndexAndArchive()
        {
            if (_archivePath.ToLower().EndsWith(ArchiveMagic.RPA_1_RPA))
            {
                _indexPath = Regex.Replace(_archivePath, @"\.rpa$", ".rpi", RegexOptions.IgnoreCase);
            }
            if (_archivePath.ToLower().EndsWith(ArchiveMagic.RPA_1_RPI))
            {
                _indexPath = _archivePath;
                _archivePath = Regex.Replace(_archivePath, @"\.rpi$", ".rpa", RegexOptions.IgnoreCase);
            }
        }

        public double CheckSupportedVersion(double version)
        {
            switch (version)
            {
                case Version.RPA_3_2:
                case Version.RPA_3:
                case Version.RPA_2:
                case Version.RPA_1:
                    // Version is OK
                    break;
                default:
                    throw new Exception("Specified version is not supported.");
            }
            
            return version;
        }

        private FileInfo GetArchiveInfo()
        {
            if (_archivePath == String.Empty)
            {
                throw new Exception("No archive file provided.");
            }

            if (!File.Exists(_archivePath))
            {
                throw new Exception("Archive file does not exist.");
            }

            return new FileInfo(_archivePath);
        }

        private FileInfo GetIndexInfo()
        {
            if (_indexPath == String.Empty)
            {
                throw new Exception("No index file provided.");
            }

            if (!File.Exists(_indexPath))
            {
                throw new Exception("Index file does not exist.");
            }

            return new FileInfo(_indexPath);
        }

        private string GetFirstLine()
        {
            using StreamReader streamReader = new StreamReader(_archivePath, Encoding.UTF8);
            return streamReader.ReadLine();
        }

        private double GetVersion()
        {
            if (_firstLine.StartsWith(ArchiveMagic.RPA_3_2))
            {
                return 3.2;
            }

            if (_firstLine.StartsWith(ArchiveMagic.RPA_3))
            {
                return 3;
            }

            if (_firstLine.StartsWith(ArchiveMagic.RPA_2))
            {
                return 2;
            }

            if (_archivePath.ToLower().EndsWith(ArchiveMagic.RPA_1_RPA) || _archivePath.ToLower().EndsWith(ArchiveMagic.RPA_1_RPI))
            {
                GetIndexAndArchive();
                if (File.Exists(_archivePath) && File.Exists(_indexPath))
                {
                    return 1;
                }
            }

            throw new Exception("File is either not valid RenPy Archive or version is not recognized.");
        }

        private string[] GetMetadata()
        {
            return _firstLine.Split(' ');
        }

        private long GetOffset()
        {
            return Convert.ToInt64(_metadata[1], 16);
        }

        private long GetObfuscationKey()
        {
            long key = 0;
            
            if (CheckVersion(ArchiveVersion, Version.RPA_3))
            {
                for(int i = 2; i < _metadata.Length; i++)
                {
                    key ^= Convert.ToInt64(_metadata[i], 16);
                }
            }
            else if (CheckVersion(ArchiveVersion, Version.RPA_3_2))
            {
                for(int i = 3; i < _metadata.Length; i++)
                {
                    key ^= Convert.ToInt64(_metadata[i], 16);
                }
            }

            return key;
        }
        
        private SortedDictionary<string,ArchiveIndex> GetIndexes()
        {
            SortedDictionary<string,ArchiveIndex> indexList = new SortedDictionary<string,ArchiveIndex>();
            object unpickledIndexes;

            string filePath = _archivePath;
            if (CheckVersion(ArchiveVersion, Version.RPA_1))
            {
                filePath = _indexPath;
            }
            
            using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath), Encoding.UTF8))
            {
                if (CheckVersion(ArchiveVersion, Version.RPA_2) || CheckVersion(ArchiveVersion, Version.RPA_3) || CheckVersion(ArchiveVersion, Version.RPA_3_2))
                {
                    reader.BaseStream.Seek(_offset, SeekOrigin.Begin);
                }

                long blockOffset = _offset;
                long blockSize = 2046;
                long payloadSize = reader.BaseStream.Length;
                byte[] fileCompressed = { };

                while (blockSize > 0)
                {
                    //long remaining = payloadSize - blockOffset;
                    if (blockOffset + blockSize > payloadSize)
                    {
                        blockSize = payloadSize - blockOffset;

                        if (blockSize < 0)
                        {
                            blockSize = 0;
                        }
                    }

                    if (blockSize != 0)
                    {
                        byte[] buffer = reader.ReadBytes((int) blockSize);
                        fileCompressed = fileCompressed.Concat(buffer).ToArray();

                        blockOffset += blockSize;
                        reader.BaseStream.Seek(blockOffset, SeekOrigin.Begin);
                    }
                }

                byte[] fileUncompressed = ZlibStream.UncompressBuffer(fileCompressed);
                using (Unpickler unpickler = new Unpickler())
                {
                    unpickledIndexes = unpickler.loads(fileUncompressed);
                }
            }
            
            // Standardize output
            foreach (DictionaryEntry kvp in (Hashtable) unpickledIndexes)
            {
                if (kvp.Value == null)
                {
                    continue;
                }

                ArchiveIndex indexEntry = new ArchiveIndex
                {
                    TreePath = (string) kvp.Key,
                    ParentPath = Path.GetDirectoryName((string) kvp.Key),
                    InArchive = true
                };
                int counter = 0;
                foreach (object[] value in (ArrayList) kvp.Value)
                { 
                    Tuples index = new Tuples
                    {
                        Offset = Convert.ToInt64(value.GetValue(0)),
                        Length = Convert.ToInt64(value.GetValue(1))
                    };
                    if ((long) value.Length == 3)
                    {
                        if (value.GetValue(2).GetType() == typeof(byte[]))
                        {
                            index.Prefix = (byte[]) value.GetValue(2);
                        }
                        else
                        {
                            index.Prefix = Encoding.UTF8.GetBytes((string) value.GetValue(2));
                        }
                    }
                    else
                    {
                        index.Prefix = Array.Empty<byte>();
                    }

                    indexEntry.Tuples.Add(counter, index);
                    counter++;
                }
                indexList.Add(indexEntry.TreePath, indexEntry);
            }

            foreach (KeyValuePair<string, ArchiveIndex> kvp in indexList)
            {
                foreach (KeyValuePair<int, Tuples> kvpI in kvp.Value.Tuples)
                {
                    // Deobfuscate index data
                    if (ArchiveVersion >= Version.RPA_3)
                    {
                        kvpI.Value.Offset ^= ObfuscationKey;
                        kvpI.Value.Length ^= ObfuscationKey;
                    }

                    kvp.Value.Length += kvpI.Value.Length;
                }
            }

            return indexList;
        }

        public SortedDictionary<string, ArchiveIndex> DeepCopyIndex(SortedDictionary<string, ArchiveIndex> originalIndex)
        {
            SortedDictionary<string, ArchiveIndex> indexCopy = new SortedDictionary<string, ArchiveIndex>();
            
            foreach (KeyValuePair<string, ArchiveIndex> kvp in originalIndex)
            {
                ArchiveIndex archIndex = new ArchiveIndex
                {
                    FullPath = kvp.Value.FullPath,
                    InArchive = kvp.Value.InArchive,
                    TreePath = kvp.Value.TreePath,
                    ParentPath = kvp.Value.ParentPath,
                    Length = kvp.Value.Length
                };
                
                foreach (KeyValuePair<int, Tuples> kvpI in kvp.Value.Tuples)
                {
                    Tuples index = new Tuples
                    {
                        Length = kvpI.Value.Length,
                        Offset = kvpI.Value.Offset,
                        Prefix = kvpI.Value.Prefix
                    };
                    
                    archIndex.Tuples.Add(kvpI.Key, index);
                }
                
                indexCopy.Add(kvp.Key, archIndex);
            }
            
            return indexCopy;
        }
        
        public string rpycInfoBanner =
            "RPYC file contains compiled RenPy code. To preview code we need to use external script called unrpyc for decompilation and Python 2.7 environment to run this script.";

        public string ParseRPYC(byte[] file)
        {
            string decompiled = String.Empty;

            if (UnrpycLocation == String.Empty)
            {
                throw new Exception(rpycInfoBanner + Environment.NewLine + Environment.NewLine + "ERROR: Location of unrpyc executable is not defined.");
            }

            if (!File.Exists(UnrpycLocation))
            {
                throw new Exception(rpycInfoBanner + Environment.NewLine + Environment.NewLine + "ERROR: Defined location of unrpyc executable cannot be found (" + UnrpycLocation + ").");
            }

            string tmpFile = Path.GetTempFileName();
            string decompiledFile = tmpFile + ".rpy"; // Use .rpy as the output file extension
            tmpFile += ".rpyc"; // Ensure tmpFile has the .rpyc extension

            try
            {
                // Write the byte array to a temporary file
                File.WriteAllBytes(tmpFile, file);

                ProcessStartInfo start = new ProcessStartInfo();
                start.FileName = UnrpycLocation; // Assume unrpycLocation points to unrpyc.exe
                start.Arguments = string.Format(@"""--try-harder"" ""{0}""", tmpFile); // Only pass the rpyc file, unrpyc will output using the same filename with .rpy by default
                start.UseShellExecute = false;
                start.RedirectStandardOutput = true;
                start.RedirectStandardError = true;
                start.CreateNoWindow = true;

                using (Process process = Process.Start(start))
                {
                    using (StreamReader reader = process.StandardOutput)
                    {
                        decompiled += reader.ReadToEnd();
                    }
                    using (StreamReader reader = process.StandardError)
                    {
                        string errorOutput = reader.ReadToEnd();
                        if (!string.IsNullOrWhiteSpace(errorOutput))
                        {
                            throw new Exception("ERROR: " + errorOutput);
                        }
                    }
                }

                // Read the decompiled output from the expected decompiled file
                decompiled = NormalizeNewLines(File.ReadAllText(decompiledFile));
            }
            catch (Exception ex)
            {
                throw new Exception("ERROR: Decompilation failed with following error:" + Environment.NewLine +
                                    Environment.NewLine + ex.Message);
            }
            finally
            {
                if (File.Exists(tmpFile))
                {
                    File.Delete(tmpFile);
                }
                if (File.Exists(decompiledFile))
                {
                    File.Delete(decompiledFile);
                }
            }

            return decompiled;
        }

        public KeyValuePair<string, byte[]> GetPreviewRaw(string fileName)
        {
            KeyValuePair<string, object> data = GetPreview(fileName, true);
            return new KeyValuePair<string, byte[]>(data.Key, (byte[]) data.Value);
        }

        public KeyValuePair<string, object> GetPreview(string fileName, bool returnRaw = false)
        {
            KeyValuePair<string, object> data = new KeyValuePair<string, object>(PreviewTypes.Unknown, null);

            if (!Index.ContainsKey(fileName))
            {
                return data;
            }

            FileInfo fileInfo = new FileInfo(fileName);
            byte[] bytes = ExtractData(fileName);

            if (ImageExtList.Contains(fileInfo.Extension.ToLower()))
            {
                if (fileInfo.Extension.ToLower() == ".avif")
                {
                    using (var memoryStream = new MemoryStream(bytes))
                    {
                        var image = SixLabors.ImageSharp.Image.Load(memoryStream);
                        data = new KeyValuePair<string, object>(PreviewTypes.Image, image.ToBitmap()); // Convert ImageSharp image to Bitmap for drawing
                    }
                }
                else if (fileInfo.Extension.ToLower() == ".webp" || Encoding.UTF8.GetString(bytes.Take(16).ToArray()).Contains("WEBP"))
                {
                    data = new KeyValuePair<string, object>(PreviewTypes.Image, new WebP().Decode(bytes));
                }
                else
                {
                    data = new KeyValuePair<string, object>(PreviewTypes.Image, Image.FromStream(new MemoryStream(bytes)));
                }
            }
            else if (TextExtList.Contains(fileInfo.Extension.ToLower()))
            {
                data = new KeyValuePair<string, object>(PreviewTypes.Text, NormalizeNewLines(Encoding.UTF8.GetString(bytes, 0, bytes.Length)));
            }
            else if (CodeExtList.Contains(fileInfo.Extension.ToLower()))
            {
                string decompiledString = ParseRPYC(bytes);

                if (decompiledString == String.Empty)
                {
                    data = new KeyValuePair<string, object>(PreviewTypes.Unknown, bytes);
                }
                else
                {
                    data = new KeyValuePair<string, object>(PreviewTypes.Text, decompiledString);
                }
            }
            else if (AudioExtList.Contains(fileInfo.Extension.ToLower()))
            {
                data = new KeyValuePair<string, object>(PreviewTypes.Audio, bytes);
            }
            else if (VideoExtList.Contains(fileInfo.Extension.ToLower()))
            {
                data = new KeyValuePair<string, object>(PreviewTypes.Video, bytes);
            }
            else
            {
                data = new KeyValuePair<string, object>(PreviewTypes.Unknown, bytes);
            }

            if (returnRaw)
            {
                data = new KeyValuePair<string, object>(data.Key, bytes);
            }

            return data;
        }

        private string NormalizeNewLines(string text)
        {
            const string winNewLine = "\r\n";
            const string linNewLine = "\n";
            const string macNewLine = "\r";
            
            int countWin = Regex.Matches(text, winNewLine).Count;
            int countLinux = Regex.Matches(text, linNewLine).Count;
            int countMac = Regex.Matches(text, macNewLine).Count;
            
            string newLineSymbol = Environment.NewLine;
            
            if (countWin >= countLinux && countWin >= countMac)
            {
                newLineSymbol = winNewLine;
            }
            else if (countLinux >= countWin && countLinux >= countMac)
            {
                newLineSymbol = linNewLine;
            }
            else if (countMac >= countWin && countMac >= countLinux)
            {
                newLineSymbol = macNewLine;
            }

            text = text.Replace(newLineSymbol, Environment.NewLine);
            
            return text;
        }

        public byte[] ExtractData(string fileName)
        {
            if (!Index.ContainsKey(fileName))
            {
                throw new Exception("Specified file does not exist in RenPy Archive.");
            }

            if (Index[fileName].InArchive)
            {
                using BinaryReader reader = new BinaryReader(File.OpenRead(_archivePath), Encoding.UTF8);
                byte[] finalData = { };

                foreach (KeyValuePair<int, Tuples> kvpI in Index[fileName].Tuples)
                {
                    reader.BaseStream.Seek(kvpI.Value.Offset, SeekOrigin.Begin);
                    byte[] prefixData = kvpI.Value.Prefix;
                    byte[] fileData = reader.ReadBytes((int) kvpI.Value.Length - kvpI.Value.Prefix.Length); // Exported file max size ~2.14 GB
                    byte[] partData = new byte[finalData.Length + prefixData.Length + fileData.Length];
                    Buffer.BlockCopy(finalData, 0, partData, 0, finalData.Length);
                    Buffer.BlockCopy(prefixData, 0, partData, finalData.Length, prefixData.Length);
                    Buffer.BlockCopy(fileData, 0, partData, finalData.Length + prefixData.Length, fileData.Length);
                    finalData = partData;
                }

                return finalData;
            }
            
            return File.ReadAllBytes(Index[fileName].FullPath);
        }

        public string Extract(string fileName, string exportPath)
        {
            byte[] finalData = ExtractData(fileName);
            string finalPath;
            if (exportPath.Trim() == String.Empty)
            {
                finalPath = ArchiveInfo.DirectoryName + @"\" + fileName;
            }
            else
            {
                if (!Directory.Exists(exportPath.Trim()))
                {
                    throw new Exception("Selected export path does not exist.");
                }
                finalPath = exportPath.Trim() + @"\" + fileName;
            }

            Directory.CreateDirectory(Path.GetDirectoryName(finalPath) ?? throw new InvalidOperationException());
            File.WriteAllBytes(finalPath, finalData);

            return ArchiveInfo.DirectoryName + @"\" + fileName;
        }

        public string SaveArchive(string archivePath)
        {
            if (archivePath.ToLower().EndsWith(".rpi"))
            {
                archivePath = Regex.Replace(archivePath, @"\.rpi$", ".rpa", RegexOptions.IgnoreCase);
            }
            
            if (!archivePath.ToLower().EndsWith(".rpa"))
            {
                archivePath += ".rpa";
            }

            string tmpPath = Regex.Replace(archivePath, @"\.rpa$", "", RegexOptions.IgnoreCase);
            tmpPath = tmpPath.Substring(0, Math.Min(100, tmpPath.Length - 1)) + "_" +
                      DateTime.Now.ToString("yyyyMMddHHmmss") + "_" + Guid.NewGuid().ToString("N");

            /*if (archivePath == _archivePath && _archivePath != String.Empty)
            {
                throw new Exception("Cannot overwrite same archive file that is loaded.");
            }*/

            string indexPath = Regex.Replace(archivePath, @"\.rpa$", ".rpi", RegexOptions.IgnoreCase);

            /*if (indexPath == _indexPath && _indexPath != String.Empty)
            {
                throw new Exception("Cannot overwrite same index file that is loaded.");
            }*/
            
            BuildArchive(archivePath, indexPath, tmpPath);

            return archivePath;
        }

        private void BuildArchive(string archivePath, string indexPath, string tmpPath)
        {
            try
            {
                if (!File.Exists(tmpPath + ".rpa"))
                {
                    File.WriteAllBytes(tmpPath + ".rpa", new byte[] { });
                }

                using (Stream stream = File.Open(tmpPath + ".rpa", FileMode.Truncate))
                {
                    int archiveOffset;
                    switch (ArchiveVersion)
                    {
                        case Version.RPA_3_2:
                            archiveOffset = 34;
                            break;
                        case Version.RPA_3:
                            archiveOffset = 34;
                            break;
                        case Version.RPA_2:
                            archiveOffset = 25;
                            break;
                        case Version.RPA_1:
                            archiveOffset = 0;
                            break;
                        default:
                            throw new Exception("Specified version is not supported.");
                    }

                    stream.Position = archiveOffset;

                    Random rnd = new Random();

                    // Update indexes
                    Hashtable indexes = new Hashtable();
                    foreach (KeyValuePair<string, ArchiveIndex> index in Index)
                    {
                        byte[] content = ExtractData(index.Key);

                        if (Padding > 0)
                        {
                            string paddingStr = String.Empty;
                            int paddingLength = rnd.Next(1, Padding);

                            while (paddingLength > 0)
                            {
                                paddingStr += Encoding.ASCII.GetString(new[] {(byte) rnd.Next(1, 255)});
                                paddingLength--;
                            }

                            byte[] paddingBytes = Encoding.ASCII.GetBytes(paddingStr);
                            archiveOffset += paddingBytes.Length;
                        }

                        stream.Position = archiveOffset;
                        stream.Write(content, 0, content.Length);

                        List<object[]> indexData = new List<object[]>();
                        if (CheckVersion(ArchiveVersion, Version.RPA_3) ||
                            CheckVersion(ArchiveVersion, Version.RPA_3_2))
                        {
                            indexData.Add(new object[]
                                {archiveOffset ^ ObfuscationKey, content.Length ^ ObfuscationKey, ""}); // Last is prefix
                        }
                        else
                        {
                            indexData.Add(new object[] {archiveOffset, content.Length});
                        }

                        archiveOffset += content.Length;

                        indexes.Add(index.Value.TreePath, indexData);
                    }

                    byte[] pickledIndexes;
                    using (Pickler pickler = new Pickler())
                    {
                        pickledIndexes = pickler.dumps(indexes);
                    }

                    byte[] fileCompressed = ZlibStream.CompressBuffer(pickledIndexes);

                    if (!CheckVersion(ArchiveVersion, Version.RPA_1))
                    {
                        stream.Position = archiveOffset;
                        stream.Write(fileCompressed, 0, fileCompressed.Length);

                        string headerContent = String.Empty;

                        switch (ArchiveVersion)
                        {
                            case Version.RPA_3_2:
                                headerContent = ArchiveMagic.RPA_3_2 + archiveOffset.ToString("x").PadLeft(16, '0') +
                                                " " +
                                                ObfuscationKey.ToString("x").PadLeft(8, '0') + "\n";
                                break;
                            case Version.RPA_3:
                                headerContent = ArchiveMagic.RPA_3 + archiveOffset.ToString("x").PadLeft(16, '0') +
                                                " " +
                                                ObfuscationKey.ToString("x").PadLeft(8, '0') + "\n";
                                break;
                            case Version.RPA_2:
                                headerContent = ArchiveMagic.RPA_2 + archiveOffset.ToString("x").PadLeft(16, '0') +
                                                "\n";
                                break;
                        }

                        byte[] headerContentByte = Encoding.UTF8.GetBytes(headerContent);

                        stream.Position = 0;
                        stream.Write(headerContentByte, 0, headerContentByte.Length);
                    }
                    else
                    {
                        File.WriteAllBytes(tmpPath + ".rpi", fileCompressed);
                    }
                }

                try
                {
                    // Test if archive is corrupted or not
                    RpaParser testParse = new RpaParser();
                    testParse.LoadArchive(tmpPath + ".rpa");
                }
                catch (Exception ex)
                {
                    throw new Exception("Validation of newly created archive failed. This usually means corrupted archive file after creation. No harm was done to original archive. Parser failed with following error during validation: " + ex.Message);
                }

                File.Copy(tmpPath + ".rpa", archivePath, true);
                File.Delete(tmpPath + ".rpa");
                if (File.Exists(tmpPath + ".rpi"))
                {
                    File.Copy(tmpPath + ".rpi", indexPath, true);
                    File.Delete(tmpPath + ".rpi");
                }
            }
            catch
            {
                if (File.Exists(tmpPath + ".rpa"))
                {
                    File.Delete(tmpPath + ".rpa");
                }
                if (File.Exists(tmpPath + ".rpi"))
                {
                    File.Delete(tmpPath + ".rpi");
                }

                throw;
            }
        }
    }

    // https://stackoverflow.com/a/28418846/3650856
    public class StructConverter
    {
        // We use this function to provide an easier way to make type-agnostic call via GetBytes method of the BitConverter class.
        // This means we can have much cleaner code below.
        private static byte[] TypeAgnosticGetBytes(object o)
        {
            switch (o)
            {
                case char c:
                    return BitConverter.GetBytes(c);
                case int i:
                    return BitConverter.GetBytes(i);
                case uint u:
                    return BitConverter.GetBytes(u);
                case long l:
                    return BitConverter.GetBytes(l);
                case ulong @ulong:
                    return BitConverter.GetBytes(@ulong);
                case short s:
                    return BitConverter.GetBytes(s);
                case ushort @ushort:
                    return BitConverter.GetBytes(@ushort);
                case byte _:
                case sbyte _:
                    return new[] { (byte)o };
                default:
                    throw new ArgumentException("Unsupported object type found");
            }
        }

        private static string GetFormatSpecifierFor(object o)
        {
            switch (o)
            {
                case char _:
                    return "c";
                case int _:
                    return "i";
                case uint _:
                    return "I";
                case long _:
                    return "q";
                case ulong _:
                    return "Q";
                case short _:
                    return "h";
                case ushort _:
                    return "H";
                case byte _:
                    return "B";
                case sbyte _:
                    return "b";
                default:
                    throw new ArgumentException("Unsupported object type found");
            }
        }

        /// <summary>
        /// Convert a byte array into an array of objects based on Python's "struct.unpack" protocol.
        /// </summary>
        /// <param name="fmt">A "struct.pack"-compatible format string</param>
        /// <param name="bytes">An array of bytes to convert to objects</param>
        /// <returns>Array of objects.</returns>
        /// <remarks>You are responsible for casting the objects in the array back to their proper types.</remarks>
        public static object[] Unpack(string fmt, byte[] bytes)
        {
            Debug.WriteLine("Format string is length {0}, {1} bytes provided.", fmt.Length, bytes.Length);

            // First we parse the format string to make sure it's proper.
            if (fmt.Length < 1) throw new ArgumentException("Format string cannot be empty.");

            bool endianFlip = false;
            if (fmt.Substring(0, 1) == "<")
            {
                Debug.WriteLine("  Endian marker found: little endian");
                // Little endian.
                // Do we need to flip endianness?
                if (BitConverter.IsLittleEndian == false) endianFlip = true;
                fmt = fmt.Substring(1);
            }
            else if (fmt.Substring(0, 1) == ">")
            {
                Debug.WriteLine("  Endian marker found: big endian");
                // Big endian.
                // Do we need to flip endianness?
                if (BitConverter.IsLittleEndian) endianFlip = true;
                fmt = fmt.Substring(1);
            }

            // Now, we find out how long the byte array needs to be
            int totalByteLength = 0;
            foreach (char c in fmt)
            {
                Debug.WriteLine("  Format character found: {0}", c);
                switch (c)
                {
                    case 'q':
                    case 'Q':
                        totalByteLength += 8;
                        break;
                    case 'i':
                    case 'I':
                        totalByteLength += 4;
                        break;
                    case 'h':
                    case 'H':
                        totalByteLength += 2;
                        break;
                    case 'b':
                    case 'B':
                    case 'x':
                        totalByteLength += 1;
                        break;
                    default:
                        throw new ArgumentException("Invalid character found in format string.");
                }
            }

            Debug.WriteLine("Endianness will {0}be flipped.", (object)(endianFlip ? "" : "NOT "));
            Debug.WriteLine("The byte array is expected to be {0} bytes long.", totalByteLength);

            // Test the byte array length to see if it contains as many bytes as is needed for the string.
            if (bytes.Length != totalByteLength) throw new ArgumentException("The number of bytes provided does not match the total length of the format string.");

            // Ok, we can go ahead and start parsing bytes!
            int byteArrayPosition = 0;
            var outputList = new List<object>();

            Debug.WriteLine("Processing byte array...");
            foreach (char c in fmt)
            {
                byte[] buf;
                switch (c)
                {
                    case 'q':
                        outputList.Add(BitConverter.ToInt64(bytes, byteArrayPosition));
                        byteArrayPosition += 8;
                        Debug.WriteLine("  Added signed 64-bit integer.");
                        break;
                    case 'Q':
                        outputList.Add(BitConverter.ToUInt64(bytes, byteArrayPosition));
                        byteArrayPosition += 8;
                        Debug.WriteLine("  Added unsigned 64-bit integer.");
                        break;
                    case 'i':
                        outputList.Add(BitConverter.ToInt32(bytes, byteArrayPosition));
                        byteArrayPosition += 4;
                        Debug.WriteLine("  Added signed 32-bit integer.");
                        break;
                    case 'I':
                        outputList.Add(BitConverter.ToUInt32(bytes, byteArrayPosition));
                        byteArrayPosition += 4;
                        Debug.WriteLine("  Added unsigned 32-bit integer.");
                        break;
                    case 'h':
                        outputList.Add(BitConverter.ToInt16(bytes, byteArrayPosition));
                        byteArrayPosition += 2;
                        Debug.WriteLine("  Added signed 16-bit integer.");
                        break;
                    case 'H':
                        if (endianFlip)
                        {
                            var deezBytes = bytes.Reverse().Skip(byteArrayPosition).Take(2).ToArray();
                            outputList.Add(BitConverter.ToUInt16(deezBytes, 0));
                        }
                        else
                        {
                            outputList.Add(BitConverter.ToUInt16(bytes, byteArrayPosition));
                        }

                        byteArrayPosition += 2;
                        Debug.WriteLine("  Added unsigned 16-bit integer.");
                        break;
                    case 'b':
                        buf = new byte[1];
                        Array.Copy(bytes, byteArrayPosition, buf, 0, 1);
                        outputList.Add((sbyte)buf[0]);
                        byteArrayPosition++;
                        Debug.WriteLine("  Added signed byte");
                        break;
                    case 'B':
                        buf = new byte[1];
                        Array.Copy(bytes, byteArrayPosition, buf, 0, 1);
                        outputList.Add(buf[0]);
                        byteArrayPosition++;
                        Debug.WriteLine("  Added unsigned byte");
                        break;
                    case 'x':
                        byteArrayPosition++;
                        Debug.WriteLine("  Ignoring a byte");
                        break;
                    default:
                        throw new ArgumentException("You should not be here.");
                }
            }
            return outputList.ToArray();
        }

        /// <summary>
        /// Convert an array of objects to a byte array, along with a string that can be used with Unpack.
        /// </summary>
        /// <param name="items">An object array of items to convert</param>
        /// <param name="littleEndian">Set to False if you want to use big endian output.</param>
        /// <param name="neededFormatStringToRecover">Variable to place an 'Unpack'-compatible format string into.</param>
        /// <returns>A Byte array containing the objects provided in binary format.</returns>
        public static byte[] Pack(object[] items, bool littleEndian, out string neededFormatStringToRecover)
        {

            // make a byte list to hold the bytes of output
            var outputBytes = new List<byte>();

            // should we be flipping bits for proper endianness?
            bool endianFlip = (littleEndian != BitConverter.IsLittleEndian);

            // start working on the output string
            string outString = (littleEndian == false ? ">" : "<");

            // convert each item in the objects to the representative bytes
            foreach (object o in items)
            {
                byte[] theseBytes = TypeAgnosticGetBytes(o);
                if (endianFlip) theseBytes = theseBytes.Reverse().ToArray();
                outString += GetFormatSpecifierFor(o);
                outputBytes.AddRange(theseBytes);
            }

            neededFormatStringToRecover = outString;

            return outputBytes.ToArray();

        }

        public static byte[] Pack(object[] items)
        {
            string dummy = "";
            return Pack(items, true, out dummy);
        }
    }
}
```
RPA_Explorer.cs:
```
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using LibVLCSharp.Shared;
using Microsoft.Win32;
using NeoSmart.PrettySize;
using RPA_Parser;

namespace RPA_Explorer
{
    public partial class RpaExplorer : Form
    {
        private RpaParser _rpaParser;
        private RpaParser _rpaParserBak;
        private Thread _operation;
        private bool _operationEnabled = true;
        private bool _archiveChanged;
        private bool _cancelAdd;
        private bool _archiveLoaded;
        private SortedDictionary<string, RpaParser.ArchiveIndex> _fileListBackup = new();
        private readonly Dictionary<string, long> _indexPathSize = new();
        private readonly List<string> _expandedList = new();
        private bool _switchTabs;
        private readonly LibVLC _libVlc; // https://code.videolan.org/mfkl/libvlcsharp-samples
        private MemoryStream _memoryStreamVlc;
        private StreamMediaInput _streamMediaInputVlc;
        private Media _mediaVlc;
        private int _searchStartIndex = 0;
        private static readonly FileInfo _appInfo = new(System.Reflection.Assembly.GetEntryAssembly()?.Location ?? throw new InvalidOperationException());
        private static readonly FileInfo _appName = new(System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name ?? throw new InvalidOperationException());
        private readonly string settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/" + _appName + @"/settings.ini";

        private static readonly System.ComponentModel.ComponentResourceManager Lang = new(typeof(Lang));

        private static Settings settings;

        public RpaExplorer()
        {
            // Form initiation
            InitializeComponent();

            Directory.CreateDirectory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/" + _appName);

            settings = new Settings(settingsPath);

            // Language initiation
            foreach (Settings.Language lang in settings.LangList)
            {
                toolStripComboBox1.Items.Add(lang.Name);
            }

            toolStripComboBox1.SelectedItem = settings.GetLang().Name;

            LoadTexts();

            // LibVLC initiation
            Core.Initialize();
            _libVlc = new LibVLC("--input-repeat=9999999"); // --input-repeat=X : Loop X times
            videoView1.MediaPlayer = new MediaPlayer(_libVlc);
            videoView1.MediaPlayer.Volume = 50;
            videoView1.MediaPlayer.TimeChanged += videoView1_MediaPlayer_TimeChanged;
            videoView1.BackgroundImage = null;
            SetMediaTimeLabel(0, 0);

            // Initialize the seek bar
            seekBar.Maximum = 1000; // Set maximum value for smooth seeking
            seekBar.Scroll += SeekBar_Scroll; // Handle scroll events for seeking

            // Subscribe to the TabControl's SelectedIndexChanged event
            tabControl1.SelectedIndexChanged += TabControl1_SelectedIndexChanged;

            // Hide the seek bar initially
            seekBar.Visible = false;

            // TreeView styles initiation
            ImageList imageList = new ImageList();
            imageList.Images.Add(Resources.treeView1_Image_folder);
            imageList.Images.Add(Resources.treeView1_Image_file);
            imageList.Images.Add(Resources.treeView1_Image_fileChanged);
            treeView1.ImageList = imageList;

            // Arguments processing
            string[] args = Environment.GetCommandLineArgs();
            if (args.Length >= 2)
            {
                LoadArchive(args[1]);
            }
        }

        private void TabControl1_SelectedIndexChanged(object sender, EventArgs e)
        {
            // Show the seek bar only on the Media tab (tabPage3)
            if (tabControl1.SelectedTab == tabPage3)
            {
                seekBar.Visible = true;
            }
            else
            {
                seekBar.Visible = false;
            }
        }

        // Event handler for the seek bar's Scroll event
        private void SeekBar_Scroll(object sender, EventArgs e)
        {
            if (videoView1.MediaPlayer?.Media != null)
            {
                long duration = videoView1.MediaPlayer.Media.Duration;
                if (duration > 0)
                {
                    // Calculate newTime based on SeekBar position
                    long newTime = (long)((seekBar.Value / (double)seekBar.Maximum) * duration);
                    newTime = Math.Min(newTime, duration - 1); // Ensure newTime does not exceed duration

                    // Update the label3 based on the new time, independent of playback state
                    SetMediaTimeLabel(duration, newTime);

                    // Seek the video. If you want to seek immediately when scrolling, uncomment the line below.
                    videoView1.MediaPlayer.Time = newTime;
                }
            }
        }

        private void videoView1_MediaPlayer_TimeChanged(object sender, MediaPlayerTimeChangedEventArgs e)
        {
            Task.Run(async () =>
            {
                if (videoView1.MediaPlayer?.Media != null)
                {
                    long duration = videoView1.MediaPlayer.Media.Duration;

                    // Retry fetching duration if it seems invalid
                    if (duration <= 0)
                    {
                        await Task.Delay(500);
                        duration = videoView1.MediaPlayer.Media.Duration;
                    }

                    if (duration > 0)
                    {
                        int progress = (int)((e.Time / (double)duration) * seekBar.Maximum);
                        progress = Math.Min(progress, seekBar.Maximum);

                        seekBar.PerformSafely(() => seekBar.Value = progress);
                        SetMediaTimeLabel(duration, e.Time);
                    }
                }
            });
        }

        // Reset the seek bar when a new video is loaded
        private void ResetPreviewFields()
        {
            pictureBox1.Image = null;
            textBox2.Text = String.Empty;
            if (_memoryStreamVlc != null)
            {
                _memoryStreamVlc.Dispose();
                _memoryStreamVlc = null;
            }
            if (_streamMediaInputVlc != null)
            {
                _streamMediaInputVlc.Dispose();
                _streamMediaInputVlc = null;
            }
            if (_mediaVlc != null)
            {
                _mediaVlc.Dispose();
                _mediaVlc = null;
            }
            if (videoView1.MediaPlayer is { IsPlaying: true })
            {
                videoView1.MediaPlayer.Play(null!);
            }
            SetMediaTimeLabel(0, 0);

            // Reset the seek bar
            seekBar.Value = 0;
        }

        private void LoadTexts()
        {
            Text = GetText("Explorer_title");
            toolStripTextBox1.Text = GetText("Language");
            button1.Text = GetText("Load_file");
            button2.Text = GetText("Export_checked");
            statusBar1.Text = GetText("Ready");
            button3.Text = GetText("Cancel_operation");
            tabPage0.Text = GetText("None");
            label2.Text = GetText("Usage_instructions_new");
            tabPage1.Text = GetText("Image");
            tabPage2.Text = GetText("Text");
            tabPage3.Text = GetText("Media");
            button4.Text = GetText("Pause");
            button5.Text = GetText("Create_new_archive");
            label4.Text = GetText("File_list");
            button6.Text = GetText("Remove_checked");
            button7.Text = GetText("Save_archive");
            button8.Text = GetText("Search_next");
            label5.Text = GetText("Search");
            optionsToolStripMenuItem.Text = GetText("Options");
            associateRPARPIExtensionsToolStripMenuItem.Text = GetText("File_association");
            aboutToolStripMenuItem.Text = GetText("About");

            GenerateArchiveInfo();
        }

        internal static string GetText(string name)
        {
            if (Lang.GetObject(settings.GetLang().Abbrev + "_" + name) != null)
            {
                return Lang.GetObject(settings.GetLang().Abbrev + "_" + name)?.ToString();
            }

            return " {!!! MISSING TRANSLATION !!!} ";
        }

        private void ExportFiles(List<string> exportFilesList, FolderBrowserDialog folderBrowserDialog)
        {
            int counter = 0;
            int jobSize = exportFilesList.Count;
            foreach (string file in exportFilesList)
            {
                counter++;
                int pctProcessed = (int)Math.Ceiling((double)counter / jobSize * 100);
                label1.PerformSafely(() => label1.Text = $@"{counter} / {jobSize}");
                progressBar1.PerformSafely(() => progressBar1.Value = pctProcessed);

                statusBar1.PerformSafely(() => statusBar1.Text = GetText("Exporting_file") + file);

                _rpaParser.Extract(file, folderBrowserDialog.SelectedPath);

                if (!_operationEnabled)
                {
                    break;
                }
            }

            button1.PerformSafely(() => button1.Enabled = true);
            button2.PerformSafely(() => button2.Enabled = true);
            button3.PerformSafely(() => button3.Enabled = false);
            progressBar1.PerformSafely(() => progressBar1.Value = 0);
            label1.PerformSafely(() => label1.Text = "");
            statusBar1.PerformSafely(() => statusBar1.Text = GetText("Ready"));

            _operationEnabled = true;
        }

        private void CreateNewArchive()
        {
            if (CheckIfChanged(GetText("Archive_modified_new")))
            {
                return;
            }

            _expandedList.Clear();
            _rpaParser = new RpaParser();

            GenerateTreeView();

            _switchTabs = true;
            tabControl1.SelectedTab = tabPage0;
            _switchTabs = false;
            label2.Text = GetText("Usage_instructions_loaded");

            ResetPreviewFields();

            treeView1.SelectedNode = null;

            button2.Enabled = true;
            button6.Enabled = true;
            button7.Enabled = true;

            statusBar1.Text = GetText("Ready");

            _archiveChanged = true;
        }

        private void SaveArchive()
        {
            if (_rpaParser.Index.Count == 0)
            {
                MessageBox.Show(GetText("Empty_archive_save"), GetText("Empty_archive"), MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            statusBar1.Text = GetText("Saving_archive");

            string initPath = _appInfo?.DirectoryName;
            if (_rpaParser.ArchiveInfo?.DirectoryName != null)
            {
                initPath = _rpaParser.ArchiveInfo.DirectoryName;
            }

            SaveFileDialog saveFileDialog = new SaveFileDialog();
            saveFileDialog.Title = GetText("Save_RenPy_Archive");
            saveFileDialog.Filter = GetText("RPA_RPI_files") + @" (*.rpa,*.rpi)|*.rpa;*.rpi";
            saveFileDialog.InitialDirectory = initPath;
            saveFileDialog.CheckFileExists = false;
            saveFileDialog.CheckPathExists = true;

            ArchiveSave options = new ArchiveSave(_rpaParser);
            options.ShowDialog();

            if (!_rpaParser.OptionsConfirmed)
            {
                return;
            }

            if (saveFileDialog.ShowDialog() == DialogResult.OK)
            {
                _rpaParserBak = _rpaParser;
                try
                {
                    string saveName = _rpaParser.SaveArchive(saveFileDialog.FileName);
                    LoadArchive(saveName, true);
                }
                catch (Exception ex)
                {
                    _rpaParser = _rpaParserBak;
                    MessageBox.Show(
                        string.Format(GetText("Save_failed_reason"), ex.Message),
                        GetText("Save_failed"), MessageBoxButtons.OK, MessageBoxIcon.Error);
                }

                GenerateTreeView();

                statusBar1.Text = GetText("Ready");
            }
        }

        private void GenerateArchiveInfo()
        {
            string archiveInfo = String.Empty;

            if (_archiveLoaded)
            {
                string selectedPath = String.Empty;
                foreach (TreeNode node in treeView1.Nodes.All())
                {
                    if (node.IsSelected)
                    {
                        selectedPath = NormalizeTreePath(node.FullPath);
                    }
                }

                long selectedSize = -1;
                int unsavedCount = 0;
                foreach (KeyValuePair<string, RpaParser.ArchiveIndex> kvp in _rpaParser.Index)
                {
                    if (!kvp.Value.InArchive)
                    {
                        unsavedCount++;
                    }

                    if (selectedPath == kvp.Key)
                    {
                        selectedSize = kvp.Value.Length;
                    }
                }

                if (_indexPathSize.ContainsKey(selectedPath))
                {
                    selectedSize = _indexPathSize[selectedPath];
                }

                if (!_rpaParser.CheckVersion(_rpaParser.ArchiveVersion, RpaParser.Version.Unknown))
                {
                    archiveInfo += GetText("Archive_version") + _rpaParser.ArchiveVersion + Environment.NewLine;
                    archiveInfo += GetText("Archive_file_location") + _rpaParser.ArchiveInfo.FullName +
                                   Environment.NewLine;
                    archiveInfo += GetText("Archive_file_size") + PrettySize.Format(_rpaParser.ArchiveInfo.Length) +
                                   Environment.NewLine;
                    if (_rpaParser.IndexInfo != null)
                    {
                        archiveInfo += GetText("Index_file_location") + _rpaParser.IndexInfo.FullName +
                                       Environment.NewLine;
                        archiveInfo += GetText("Index_file_size") + PrettySize.Format(_rpaParser.IndexInfo.Length) +
                                       Environment.NewLine;
                    }
                }

                archiveInfo += GetText("Files_count") + _rpaParser.Index.Count + Environment.NewLine;
                archiveInfo += GetText("Unsaved_files_count") + unsavedCount + Environment.NewLine;

                if (selectedSize != -1)
                {
                    if (selectedPath == String.Empty)
                    {
                        selectedPath = "/";
                    }
                    archiveInfo += GetText("Selected_file_path") + selectedPath +
                                   Environment.NewLine;
                    archiveInfo += GetText("Selected_file_size") + PrettySize.Format(selectedSize) +
                                   Environment.NewLine;
                }
            }

            textBox1.Text = archiveInfo.Trim();
        }

        private void LoadArchive(string openFile, bool ignoreChanges = false)
        {
            if (!ignoreChanges)
            {
                if (CheckIfChanged(GetText("Archive_modified_load")))
                {
                    return;
                }
            }

            using OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.Title = GetText("Load_RenPy_Archive");
            openFileDialog.Filter = GetText("RPA_RPI_files") + @" (*.rpa,*.rpi)|*.rpa;*.rpi";

            string archivePath = settings.GetArchive();
            if (!string.IsNullOrEmpty(archivePath))
            {
                FileInfo fi = new FileInfo(archivePath);
                openFileDialog.InitialDirectory = fi.DirectoryName;
            }

            DialogResult dialogResult = DialogResult.None;
            if (string.IsNullOrEmpty(openFile))
            {
                dialogResult = openFileDialog.ShowDialog();
            }
            else
            {
                if (openFile.EndsWith(".rpa") || openFile.EndsWith(".rpi"))
                {
                    dialogResult = DialogResult.OK;
                    openFileDialog.FileName = openFile;
                }
                else
                {
                    MessageBox.Show(
                        string.Format(GetText("Load_failed_reason"), GetText("Not_valid_archive_file")),
                        GetText("Load_failed"), MessageBoxButtons.OK, MessageBoxIcon.Error);
                    statusBar1.Text = GetText("Ready");
                    return;
                }
            }

            if (dialogResult == DialogResult.OK)
            {
                if (openFileDialog.CheckFileExists)
                {
                    settings.SetArchive(openFileDialog.FileName);
                    statusBar1.Text = GetText("Loading_file") + openFileDialog.FileName;

                    try
                    {
                        _expandedList.Clear();
                        _rpaParser = new RpaParser();

                        string appFolder = Path.GetDirectoryName(Application.ExecutablePath);
                        _rpaParser.UnrpycLocation = Path.Combine(appFolder, "unrpyc.exe");

                        _rpaParser.LoadArchive(openFileDialog.FileName);
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show(
                            string.Format(GetText("Load_failed_reason"), ex.Message),
                            GetText("Load_failed"), MessageBoxButtons.OK, MessageBoxIcon.Error);
                        statusBar1.Text = GetText("Ready");
                        return;
                    }

                    GenerateTreeView();

                    _switchTabs = true;
                    tabControl1.SelectedTab = tabPage0;
                    _switchTabs = false;
                    label2.Text = GetText("Usage_instructions_loaded");

                    ResetPreviewFields();

                    treeView1.SelectedNode = null;

                    button2.Enabled = true;
                    button6.Enabled = true;
                    button7.Enabled = true;

                    statusBar1.Text = GetText("Ready");
                }

                _archiveChanged = false;
            }
        }

        private void AddFilesToArchive(string[] pathList)
        {
            // TODO: prompt for path to inset files into? Default can be root or selected object?
            //       Or don't prompt and use selected object? Empty folders should be excluded.
            //string promptValue = Prompt.ShowDialog("Test", "123", "xx");

            _fileListBackup.Clear();
            _fileListBackup = _rpaParser.DeepCopyIndex(_rpaParser.Index);
            _cancelAdd = false;

            foreach (string path in pathList)
            {
                string originalPath = path;
                if (Directory.Exists(path))
                {
                    originalPath = new DirectoryInfo(path).Parent?.FullName;
                }
                if (File.Exists(path))
                {
                    originalPath = new FileInfo(path).DirectoryName;
                }
                AddPathToIndex(path, originalPath);
            }

            if (!_cancelAdd)
            {
                _rpaParser.Index = _rpaParser.DeepCopyIndex(_fileListBackup);
            }

            _fileListBackup.Clear();
            GenerateTreeView();
        }

        private void GenerateTreeView()
        {
            _indexPathSize.Clear();
            treeView1.Nodes.Clear();
            TreeNode root = new TreeNode();
            TreeNode node;
            root.Name = "";
            root.Text = @"/";
            root.BackColor = Color.SandyBrown;
            root.ImageIndex = 0;
            treeView1.Nodes.Add(root);
            _indexPathSize.Add("", 0);
            string pathBuild;

            // Folders first
            foreach (KeyValuePair<string, RpaParser.ArchiveIndex> kvp in _rpaParser.Index)
            {
                node = root;
                pathBuild = String.Empty;
                foreach (string pathBits in kvp.Key.Split('/'))
                {
                    if (pathBuild == String.Empty)
                    {
                        pathBuild = pathBits;
                    }
                    else
                    {
                        pathBuild += "/" + pathBits;
                    }

                    if (node.Nodes.ContainsKey(pathBits))
                    {
                        node = node.Nodes[pathBits];
                    }
                    else
                    {
                        if (pathBuild != kvp.Key)
                        {
                            node = node.Nodes.Add(pathBits, pathBits);
                        }
                    }
                }
            }

            // Files second
            foreach (KeyValuePair<string, RpaParser.ArchiveIndex> kvp in _rpaParser.Index)
            {
                node = root;
                pathBuild = String.Empty;
                foreach (string pathBits in kvp.Key.Split('/'))
                {
                    if (pathBuild == String.Empty)
                    {
                        pathBuild = pathBits;
                    }
                    else
                    {
                        pathBuild += "/" + pathBits;
                    }

                    node = node.Nodes.ContainsKey(pathBits) ? node.Nodes[pathBits] : node.Nodes.Add(pathBits, pathBits);

                    if (pathBuild != kvp.Key)
                    {
                        if (!_indexPathSize.ContainsKey(pathBuild))
                        {
                            _indexPathSize.Add(pathBuild, 0);
                        }

                        if (_rpaParser.Index.ContainsKey(kvp.Key))
                        {
                            _indexPathSize[pathBuild] += _rpaParser.Index[kvp.Key].Length;
                        }
                    }
                    else
                    {
                        _indexPathSize[""] += _rpaParser.Index[kvp.Key].Length;
                    }
                }
            }

            foreach (TreeNode nodeVisuals in treeView1.Nodes.All())
            {
                string nodeName = NormalizeTreePath(nodeVisuals.FullPath);

                if (nodeName != String.Empty)
                {
                    if (nodeVisuals.Nodes.Count > 0)
                    {
                        nodeVisuals.BackColor = Color.SandyBrown;
                        nodeVisuals.ImageIndex = 0;
                    }
                    else
                    {
                        nodeVisuals.BackColor = Color.Transparent;
                        nodeVisuals.ImageIndex = 1;
                    }
                }

                if (_rpaParser.Index.ContainsKey(nodeName))
                {
                    if (!_rpaParser.Index[nodeName].InArchive)
                    {
                        nodeVisuals.ForeColor = Color.Green;
                        nodeVisuals.ImageIndex = 2;

                        MarkChanged(nodeVisuals);
                    }
                }

                if (_expandedList.Contains(nodeVisuals.FullPath))
                {
                    nodeVisuals.Expand();
                }
            }

            root.Expand();
            _archiveLoaded = true;

            GenerateArchiveInfo();
        }

        private void MarkChanged(TreeNode node)
        {
            if (node.Parent != null)
            {
                node.Parent.ForeColor = Color.Green;
                MarkChanged(node.Parent);
            }
        }

        private void Search(TextBox tb, string pattern)
        {
            int index;

            tb.Focus();

            if ((index = tb.Text.IndexOf(pattern, _searchStartIndex, StringComparison.Ordinal)) != -1)
            {
                tb.Select(index, pattern.Length);
                _searchStartIndex = tb.SelectionStart + tb.SelectionLength;
            }
            else
            {
                _searchStartIndex = 0;
                if ((index = tb.Text.IndexOf(pattern, _searchStartIndex, StringComparison.Ordinal)) != -1)
                {
                    tb.Select(index, pattern.Length);
                    _searchStartIndex = tb.SelectionStart + tb.SelectionLength;
                }
                else
                {
                    tb.Select(0, 0);
                }
            }

            tb.ScrollToCaret();
        }

        private string NormalizeTreePath(string path)
        {
            return Regex.Replace(path, "^/+", "");
        }

        private async void PreviewSelectedItem()
        {
            TreeNode selectedNode = new TreeNode();
            bool unsupportedFile = true;
            pictureBox1.Image = null;

            foreach (TreeNode node in treeView1.Nodes.All())
            {
                // Reset fields
                ResetPreviewFields();

                if (node.IsSelected)
                {
                    selectedNode = node;
                }

                if (node.IsSelected && _rpaParser.Index.ContainsKey(NormalizeTreePath(node.FullPath)))
                {
                    KeyValuePair<string, object> data = new KeyValuePair<string, object>();
                    try
                    {
                        data = await Task.Run(() => _rpaParser.GetPreview(NormalizeTreePath(node.FullPath)));

                        if (data.Key == RpaParser.PreviewTypes.Image || data.Key == RpaParser.PreviewTypes.AVIF)
                        {
                            if (data.Key == RpaParser.PreviewTypes.AVIF)
                            {
                                // Assuming data.Value is a byte array for AVIF
                                using (var stream = new MemoryStream((byte[])data.Value))
                                {
                                    using (var image = SixLabors.ImageSharp.Image.Load(stream))
                                    {
                                        pictureBox1.Image = image.ToBitmap(); // Convert to Bitmap for PictureBox
                                    }
                                }
                            }
                            else
                            {
                                pictureBox1.Image = (Image)data.Value;
                            }
                            _switchTabs = true;
                            tabControl1.SelectedTab = tabPage1;
                            _switchTabs = false;
                            unsupportedFile = false;
                        }
                        else if (data.Key == RpaParser.PreviewTypes.Text)
                        {
                            _searchStartIndex = 0;
                            textBox2.Text = (string)data.Value;
                            _switchTabs = true;
                            tabControl1.SelectedTab = tabPage2;
                            _switchTabs = false;
                            unsupportedFile = false;
                        }
                        else if (data.Key == RpaParser.PreviewTypes.Audio || data.Key == RpaParser.PreviewTypes.Video)
                        {
                            // Handle audio and video
                        }
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show(string.Format(GetText("Preview_failed_reason"), ex.Message), GetText("Preview_failed"), MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }

                    break;
                }
            }

            if (unsupportedFile)
            {
                _switchTabs = true;
                tabControl1.SelectedTab = tabPage0;
                _switchTabs = false;
                label2.Text = GetText("Preview_is_not_supported");
            }

            treeView1.SelectedNode = selectedNode;
            treeView1.SelectedNode.SelectedImageIndex = selectedNode.ImageIndex;
            treeView1.SelectedNode.EnsureVisible();
            treeView1.Focus();
        }

        private void CheckAllChildNodes(TreeNode node, bool isChecked)
        {
            if (node.Nodes.Count == 0)
            {
                node.Checked = isChecked;
            }
            else
            {
                foreach (TreeNode childNode in node.Nodes)
                {
                    CheckAllChildNodes(childNode, isChecked);
                }
                node.Checked = isChecked;
            }
        }

        private void ParentCheckControl(TreeNode parentNode)
        {
            foreach (TreeNode node in parentNode.Nodes.All())
            {
                if (!node.Checked)
                {
                    parentNode.Checked = false;
                    break;
                }
                parentNode.Checked = true;
            }
            if (parentNode.Parent != null)
            {
                ParentCheckControl(parentNode.Parent);
            }
        }

        private void PlayPauseMedia()
        {
            if (videoView1.MediaPlayer is { IsPlaying: true })
            {
                videoView1.MediaPlayer?.Pause();
                button4.Text = GetText("Play");
            }
            else
            {
                videoView1.MediaPlayer?.Play();
                button4.Text = GetText("Pause");
            }
        }

        private void SetMediaTimeLabel(long totalTimeMillies, long currentTimeMillies)
        {
            TimeSpan totalTime = TimeSpan.FromMilliseconds(totalTimeMillies);
            TimeSpan currentTime = TimeSpan.FromMilliseconds(currentTimeMillies);
            TimeSpan remainingTime = totalTime - currentTime;

            string timeFormat = @"hh\:mm\:ss";
            string timeText = $"{currentTime.ToString(timeFormat)} / {totalTime.ToString(timeFormat)} (-{remainingTime.ToString(timeFormat)})";

            label3.PerformSafely(() => label3.Text = timeText);
        }

        private bool CheckIfChanged(string message)
        {
            if (message != String.Empty && _archiveChanged)
            {
                if (MessageBox.Show(message, GetText("Archive_modified"), MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No)
                {
                    return true;
                }

                return false;
            }

            return _archiveChanged;
        }

        private void AddPathToIndex(string path, string originalPath)
        {
            _archiveChanged = true;
            RpaParser.ArchiveIndex index = new RpaParser.ArchiveIndex
            {
                InArchive = false
            };

            if (Directory.Exists(path))
            {
                foreach (string pathFile in Directory.GetFiles(path))
                {
                    AddPathToIndex(pathFile, originalPath);
                }
                foreach (string pathDir in Directory.GetDirectories(path))
                {
                    AddPathToIndex(pathDir, originalPath);
                }
            }

            if (File.Exists(path) && !_cancelAdd)
            {
                index.FullPath = path.Replace(@"\", "/");
                index.TreePath = index.FullPath.Replace(originalPath.Replace(@"\", "/") + "/", String.Empty);
                index.ParentPath = Path.GetDirectoryName(index.TreePath);
                index.Length = new FileInfo(path).Length;
                index.InArchive = false;

                if (_fileListBackup.ContainsKey(index.TreePath))
                {
                    _fileListBackup.Remove(index.TreePath);
                }

                _fileListBackup.Add(index.TreePath, index);
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            LoadArchive(String.Empty);
        }

        private void button2_Click(object sender, EventArgs e)
        {
            using FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog();
            List<string> exportFilesList = new List<string>();
            foreach (TreeNode node in treeView1.Nodes.All())
            {
                if (node.Checked && _rpaParser.Index.ContainsKey(NormalizeTreePath(node.FullPath)))
                {
                    exportFilesList.Add(NormalizeTreePath(node.FullPath));
                }
            }

            if (exportFilesList.Count == 0)
            {
                return;
            }

            folderBrowserDialog.SelectedPath = _rpaParser.ArchiveInfo.DirectoryName;
            if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
            {
                button1.Enabled = false;
                button2.Enabled = false;
                button3.Enabled = true;
                progressBar1.Value = 0;
                label1.PerformSafely(() => label1.Text = "");

                _operation = new Thread(() => ExportFiles(exportFilesList, folderBrowserDialog));
                _operation.Start();
            }
        }

        public class FileAssociation
        {
            public string Extension { get; set; }
            public string ProgId { get; set; }
            public string FileTypeDescription { get; set; }
            public string ExecutableFilePath { get; set; }
        }

        public class FileAssociations
        {
            // needed so that Explorer windows get refreshed after the registry is updated
            [DllImport("Shell32.dll")]
            private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2);

            private const int SHCNE_ASSOCCHANGED = 0x8000000;
            private const int SHCNF_FLUSH = 0x1000;


            public static void EnsureAssociationsSet(params FileAssociation[] associations)
            {
                bool madeChanges = false;
                foreach (FileAssociation association in associations)
                {
                    madeChanges |= SetAssociation(
                        association.Extension,
                        association.ProgId,
                        association.FileTypeDescription,
                        association.ExecutableFilePath);
                }

                if (madeChanges)
                {
                    SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero);
                }
            }

            private static bool SetAssociation(string extension, string progId, string fileTypeDescription, string applicationFilePath)
            {
                bool madeChanges = false;
                madeChanges |= SetKeyDefaultValue(@"Software\Classes\" + extension, progId);
                madeChanges |= SetKeyDefaultValue(@"Software\Classes\" + progId, fileTypeDescription);
                madeChanges |= SetKeyDefaultValue($@"Software\Classes\{progId}\shell\open\command", "\"" + applicationFilePath + "\" \"%1\"");
                return madeChanges;
            }

            private static bool SetKeyDefaultValue(string keyPath, string value)
            {
                using var key = Registry.CurrentUser.CreateSubKey(keyPath);
                if (key == null || key.GetValue(null) as string == value) return false;
                key.SetValue(null, value);
                return true;
            }
        }

        private void button3_Click(object sender, EventArgs e)
        {
            _operationEnabled = false;
        }

        private void treeView1_AfterSelect(object sender, EventArgs e)
        {
            PreviewSelectedItem();
            GenerateArchiveInfo();
        }

        private void treeView1_AfterCheck(object sender, TreeViewEventArgs e)
        {
            if (e.Action != TreeViewAction.Unknown)
            {
                Task.Run(async () =>
                {
                    await Task.Delay(50); // Prevent rapid selection from missing updates
                    Application.DoEvents(); // Force UI refresh to process all pending events

                    // Ensure all children are updated
                    if (e.Node.Nodes.Count > 0)
                    {
                        CheckAllChildNodes(e.Node, e.Node.Checked);
                    }

                    // Ensure parents reflect the correct state
                    if (e.Node.Parent != null)
                    {
                        ParentCheckControl(e.Node.Parent);
                    }
                });
            }
        }

        private void tabControl1_Selecting(object sender, TabControlCancelEventArgs e)
        {
            if (!_switchTabs)
            {
                e.Cancel = true;
            }
            _switchTabs = false;
        }

        private void button4_Click(object sender, EventArgs e)
        {
            PlayPauseMedia();
        }

        private void trackBar1_Scroll(object sender, EventArgs e)
        {
            if (videoView1.MediaPlayer?.Media != null)
            {
                videoView1.MediaPlayer.Volume = trackBar1.Value;
            }
        }

        private void RpaExplorer_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (CheckIfChanged(GetText("Archive_modified_close")))
            {
                e.Cancel = true;
            }
        }

        private void button5_Click(object sender, EventArgs e)
        {
            CreateNewArchive();
        }

        private string targetFoldername; // Class-level variable to store the folder name

        private void treeView1_DragDrop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                string[] droppedFiles = (string[])e.Data.GetData(DataFormats.FileDrop);

                // Initialize the targetFoldername variable
                targetFoldername = string.Empty; // Reset before each drop

                // Check if the first dropped item is a directory
                if (Directory.Exists(droppedFiles[0]))
                {
                    // If it's a folder, set targetFoldername to the name of that folder
                    targetFoldername = Path.GetFileName(droppedFiles[0]);
                }

                if (e.Data.GetDataPresent(DataFormats.FileDrop))
                {
                    AddFilesToArchive((string[])e.Data.GetData(DataFormats.FileDrop));
                }
            }
        }

        private void treeView1_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop) && _archiveLoaded)
            {
                e.Effect = DragDropEffects.Copy;
                return;
            }
            e.Effect = DragDropEffects.None;
        }

        private void tabControl1_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effect = DragDropEffects.Copy;
                return;
            }
            e.Effect = DragDropEffects.None;
        }

        private void tabControl1_DragDrop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                LoadArchive(((string[])e.Data.GetData(DataFormats.FileDrop))[0]);
            }
        }

        private void button6_Click(object sender, EventArgs e)
        {
            bool changed = false;
            foreach (TreeNode node in treeView1.Nodes.All())
            {
                if (node.Checked && _rpaParser.Index.ContainsKey(NormalizeTreePath(node.FullPath)))
                {
                    _rpaParser.Index.Remove(NormalizeTreePath(node.FullPath));
                    changed = true;
                }
            }

            if (changed)
            {
                _archiveChanged = true;
                GenerateTreeView();
            }
        }

        private void button7_Click(object sender, EventArgs e)
        {
            SaveArchive();
        }

        private void treeView1_BeforeCollapse(object sender, TreeViewCancelEventArgs e)
        {
            if (_expandedList.Contains(e.Node.FullPath))
            {
                _expandedList.Remove(e.Node.FullPath);
            }
        }

        private void treeView1_BeforeExpand(object sender, TreeViewCancelEventArgs e)
        {
            _expandedList.Add(e.Node.FullPath);
        }

        private void toolStripComboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            settings.SetLang(toolStripComboBox1.SelectedItem.ToString());
            LoadTexts();
        }

        private void button8_Click(object sender, EventArgs e)
        {
            if (textBox3.Text.Trim() != String.Empty)
            {
                Search(textBox2, textBox3.Text.Trim());
            }
        }

        private void associateRPARPIExtensionsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            ProcessModule processModule = Process.GetCurrentProcess().MainModule;
            if (processModule != null)
            {
                string filePath = processModule.FileName;
                FileAssociations.EnsureAssociationsSet(
                    new FileAssociation
                    {
                        Extension = ".rpi",
                        ProgId = "RPA Explorer",
                        FileTypeDescription = "RenPy Index File",
                        ExecutableFilePath = filePath
                    },
                    new FileAssociation
                    {
                        Extension = ".rpa",
                        ProgId = "RPA Explorer",
                        FileTypeDescription = "RenPy Archive File",
                        ExecutableFilePath = filePath
                    });
            }
        }

        private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
        {
            new About().ShowDialog();
        }
    }

    public static class Prompt
    {
        public static string ShowDialog(string text, string caption, string defaultValue)
        {
            Form prompt = new Form
            {
                Width = 500,
                Height = 150,
                FormBorderStyle = FormBorderStyle.FixedDialog,
                Text = caption,
                StartPosition = FormStartPosition.CenterScreen,
                MaximizeBox = false,
                MinimizeBox = false,
                TopMost = true
            };
            Label textLabel = new Label { Left = 50, Top = 20, Text = text };
            TextBox textBox = new TextBox { Left = 50, Top = 50, Width = 400, Text = defaultValue };
            Button confirmation = new Button { Text = @"OK", Left = 350, Width = 101, Top = 70, DialogResult = DialogResult.OK };
            confirmation.Click += (sender, e) => { prompt.Close(); };
            prompt.Controls.Add(textBox);
            prompt.Controls.Add(confirmation);
            prompt.Controls.Add(textLabel);
            prompt.AcceptButton = confirmation;

            return prompt.ShowDialog() == DialogResult.OK ? textBox.Text : String.Empty;
        }
    }

    public static class CrossThreadExtensions
    {
        public static void PerformSafely(this Control target, Action action)
        {
            if (target.InvokeRequired)
            {
                target.Invoke(action);
            }
            else
            {
                action();
            }
        }

        public static void PerformSafely<T1>(this Control target, Action<T1> action, T1 parameter)
        {
            if (target.InvokeRequired)
            {
                target.Invoke(action, parameter);
            }
            else
            {
                action(parameter);
            }
        }

        public static void PerformSafely<T1, T2>(this Control target, Action<T1, T2> action, T1 p1, T2 p2)
        {
            if (target.InvokeRequired)
            {
                target.Invoke(action, p1, p2);
            }
            else
            {
                action(p1, p2);
            }
        }
    }


    public static class Extensions
    {
        public static IEnumerable<TreeNode> All(this TreeNodeCollection nodes)
        {
            foreach (TreeNode n in nodes)
            {
                yield return n;
                foreach (TreeNode child in n.Nodes.All())
                    yield return child;
            }
        }
    }
}
```
Имя "SixLabors" не существует в текущем контексте.
"RpaParser.PreviewTypes" не содержит определение для "AVIF".
"RpaParser.PreviewTypes" не содержит определение для "AVIF".
Имя "SixLabors" не существует в текущем контексте.