/// <summary>
 /// Obtains the markup of an AI's difficulty.
 /// </summary>
 /// <param name="scalar">Garanteed index of difficulty. (0 = easy, 1 = medium, 2 = hard)</param>
 /// <param name="saveAt">Location to save markup at.</param>
 public void GetAIDifficultyMarkup(int scalar, string saveAt)
 {
     using (cg.LockHandler.Passive)
     {
         cg.UpdateScreen();
         int[]        scales = new int[] { 33, 49, 34 };
         DirectBitmap tmp    = Capture.Clone(401, 244, scales[scalar], 17);
         for (int x = 0; x < tmp.Width; x++)
         {
             for (int y = 0; y < tmp.Height; y++)
             {
                 if (tmp.CompareColor(x, y, Colors.WHITE, 30))
                 {
                     tmp.SetPixel(x, y, Color.Black);
                 }
                 else
                 {
                     tmp.SetPixel(x, y, Color.White);
                 }
             }
         }
         tmp.Save(saveAt);
         tmp.Dispose();
     }
 }
 /// <summary>
 /// Disposes data used by the Identity object.
 /// </summary>
 public void Dispose()
 {
     Disposed = true;
     if (!Disposed && IdentityMarkup != null)
     {
         IdentityMarkup.Dispose();
     }
 }
Esempio n. 3
0
        public Bitmap CloneAsBitmap(int x, int y, int width, int height)
        {
            DirectBitmap dbClone = Clone(x, y, width, height);
            Bitmap       bmp     = dbClone.ToBitmap();

            dbClone.Dispose();
            return(bmp);
        }
 private static void ProcessCreateError(List <Tuple <string, string> > initialSettings, OverwatchInfoManual info, Process process, DirectBitmap bmp, Exception ex)
 {
     if (info.CloseOverwatchProcessOnFailure)
     {
         process.CloseMainWindow();
         process.Close();
     }
     if (bmp != null)
     {
         bmp.Dispose();
     }
     RestoreVideoSettings(info.OverwatchSettingsFilePath, initialSettings);
     throw ex;
 }
        /// <summary>
        /// Disposes of all resources being used by the CustomGame instance.
        /// </summary>
        public void Dispose()
        {
            using (LockHandler.Interactive)
            {
                Disposed = true;
                Commands.StopScanning();
                PersistentScan = false;

                if (Capture != null)
                {
                    Capture.Dispose();
                }
            }
        }
Esempio n. 6
0
        /// <summary>
        /// Disposes of all resources being used by the CustomGame instance.
        /// </summary>
        public void Dispose()
        {
            if (Disposed)
            {
                return;
            }

            Commands.StopScanning();
            PersistentScan = false;
            PersistentScanningTask.Wait();

            if (Capture != null)
            {
                Capture.Dispose();
            }

            Disposed = true;
        }
Esempio n. 7
0
        private static void ScreenshotBitBlt(IntPtr hWnd, ref DirectBitmap capture)
        {
            try
            {
                // get the hDC of the target window
                IntPtr hdcSrc = User32.GetDC(hWnd);
                // get the size
                Rectangle windowRect = new Rectangle();
                User32.GetWindowRect(hWnd, ref windowRect);
                int width  = windowRect.Right - windowRect.Left;
                int height = windowRect.Bottom - windowRect.Top;
                // create a device context we can copy to
                IntPtr hdcDest = Gdi32.CreateCompatibleDC(hdcSrc);
                // create a bitmap we can copy it to,
                // using GetDeviceCaps to get the width/height
                IntPtr hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height);
                // select the bitmap object
                IntPtr hOld = Gdi32.SelectObject(hdcDest, hBitmap);
                // bitblt over
                Gdi32.BitBlt(hdcDest, 1, 31, width - 10, height, hdcSrc, 0, 0, (uint)Gdi32.TernaryRasterOperations.SRCCOPY | (uint)Gdi32.TernaryRasterOperations.CAPTUREBLT);
                // restore selection
                Gdi32.SelectObject(hdcDest, hOld);

                if (capture != null)
                {
                    capture.Dispose();
                }
                capture = new DirectBitmap(hdcSrc, hBitmap);

                // clean up
                Gdi32.DeleteDC(hdcDest);
                User32.ReleaseDC(hWnd, hdcSrc);
                // free up the Bitmap object
                Gdi32.DeleteObject(hBitmap);
            }
            catch (ExternalException)
            {
                // Failed to capture window, usually because it was closed.
#if DEBUG
                CustomGameDebug.WriteLine("Failed to capture window. Is it closed?");
#endif
            }
        }
Esempio n. 8
0
        private static void ScreenshotScreenCopy(IntPtr hWnd, ref DirectBitmap capture)
        {
            Rectangle rect = new Rectangle();

            User32.GetWindowRect(hWnd, ref rect);

            int width  = rect.Right - rect.Left;
            int height = rect.Bottom - rect.Top;

            if (capture != null)
            {
                capture.Dispose();
            }
            Bitmap   bmp = new Bitmap(width, height);
            Graphics g   = Graphics.FromImage(bmp);

            g.CopyFromScreen(rect.Left - 7, rect.Top, -14, 0, new Size(width, height), CopyPixelOperation.SourceCopy);
            g.Dispose();
            capture = new DirectBitmap(bmp);
            bmp.Dispose();
        }
        private void ScanCommands()
        {
            while (KeepScanning)
            {
                // Wait for listen to equal true
                Thread.Sleep(5);
                if (!Listen || ListenTo.Count == 0)
                {
                    continue;
                }

                try
                {
                    using (cg.LockHandler.SemiInteractive)
                    {
                        cg.UpdateScreen();

                        // Check if the chat updated
                        #region Check For Chat Update
                        DirectBitmap chatMarkup = Capture.Clone(Rectangles.LOBBY_CHATBOX);

                        if (PreviousChatMarkup == null)
                        {
                            PreviousChatMarkup = chatMarkup;
                        }
                        else
                        {
                            if (PreviousChatMarkup.CompareTo(chatMarkup, 5, 98, DBCompareFlags.Multithread))
                            {
                                chatMarkup.Dispose();
                                continue;
                            }
                            else
                            {
                                PreviousChatMarkup.Dispose();
                                PreviousChatMarkup = chatMarkup;
                            }
                        }
                        #endregion

                        foreach (LineInfo line in lineInfo)
                        {
                            foreach (int[] color in Chat.ChatColors)
                            {
                                if (Capture.CompareColor(MarkerX, line.Marker, color, Chat.ChatFade))
                                {
                                    string command    = "";
                                    int    nameLength = -1;
#if DEBUG
                                    List <LetterResult> letterInfos = new List <LetterResult>();
#endif

                                    for (int i = 0; i < line.Lines.Length; i++)
                                    {
                                        LineScanResult linescan = ScanLine(TextStart, ChatLength, line.Lines[i], color);

                                        command += linescan.Word;

#if DEBUG
                                        letterInfos.AddRange(linescan.LetterInfos);
#endif

                                        if (i == 0)
                                        {
                                            nameLength = linescan.NameLength;
                                        }
                                    }

                                    AddExecutedCommand(line.Lines[0], nameLength, color, command
#if DEBUG
                                                       , letterInfos
#endif
                                                       );

                                    break;
                                }
                            }
                        }
                    }
                }
                catch (OverwatchClosedException) { }
            } // while

            // Dispose of resources used by this class.
            if (PreviousChatMarkup != null)
            {
                PreviousChatMarkup.Dispose();
            }
        }
        /// <summary>
        /// Creates an Overwatch process using the currently logged in battle.net account.
        /// </summary>
        /// <param name="processInfo">Parameters for creating the process.</param>
        /// <returns>The created Overwatch process.</returns>
        public static Process StartOverwatch(OverwatchInfoAuto processInfo = null)
        {
            if (processInfo == null)
            {
                processInfo = new OverwatchInfoAuto();
            }

            if (!File.Exists(processInfo.BattlenetExecutableFilePath))
            {
                throw new FileNotFoundException(string.Format("Battle.net.exe's executable at {0} was not found. " +
                                                              "Change battlenetExeLocation to the location of the battle.net.exe executable.", processInfo.BattlenetExecutableFilePath));
            }

            Stopwatch startTime = new Stopwatch();

            // If battle.net is not started, start it.
            if (Process.GetProcessesByName("battle.net").Length == 0)
            {
#if DEBUG
                CustomGameDebug.WriteLine("No battle.net process found, starting battle.net.");
#endif

                Process battlenet = new Process();
                battlenet.StartInfo.FileName = processInfo.BattlenetExecutableFilePath;
                battlenet.Start();

                startTime.Start();
                // The battle.net app is fully started when there are 3 battle.net processes. Loop while there are less than 3.
                while (Process.GetProcessesByName("battle.net").Length < 3)
                {
                    if (startTime.ElapsedMilliseconds >= processInfo.MaxBattlenetStartTime || processInfo.MaxBattlenetStartTime == -1)
                    {
#if DEBUG
                        CustomGameDebug.WriteLine("Error: Battle.net took too long to start.");
#endif
                        throw new OverwatchStartFailedException("Battle.net took too long to start.");
                    }
                    Thread.Sleep(200);
                }
#if DEBUG
                CustomGameDebug.WriteLine("Finished starting Battle.net.");
#endif
            }
#if DEBUG
            else
            {
                CustomGameDebug.WriteLine("Battle.net process found.");
            }

            CustomGameDebug.WriteLine("Starting the Overwatch process.");
#endif

            Process[] processList = Process.GetProcessesByName("Overwatch");

            // Set the video settings.
            var initialSettings = ChangeVideoSettings(processInfo.OverwatchSettingsFilePath, VideoSettings.Item1, VideoSettings.Item2);

            Process battlenetOW = new Process();
            // The arguments to start the game directly before August 2018:
            // battlenet.StartInfo.FileName = "battlenet://Pro";
            // The arguments after:
            battlenetOW.StartInfo.FileName  = processInfo.BattlenetExecutableFilePath;
            battlenetOW.StartInfo.Arguments = "--exec=\"launch Pro\"";
            battlenetOW.Start();

            startTime.Restart();

            while (startTime.ElapsedMilliseconds < processInfo.MaxOverwatchStartTime || processInfo.MaxOverwatchStartTime == -1)
            {
                Process[] newProcessList = Process.GetProcessesByName("Overwatch");

                for (int i = 0; i < newProcessList.Length; i++)
                {
                    if (processList.Contains(newProcessList[i]) == false)
                    {
                        Process owProcess = newProcessList[i];

                        WaitForVisibleProcessWindow(owProcess);
                        RestoreVideoSettings(processInfo.OverwatchSettingsFilePath, initialSettings);

                        if (processInfo.AutomaticallyCreateCustomGame)
                        {
                            DirectBitmap bmp = null;
                            if (WaitForMainMenu(processInfo.ScreenshotMethod, owProcess.MainWindowHandle, bmp, processInfo.MaxWaitForMenuTime))
                            {
#if DEBUG
                                CustomGameDebug.WriteLine("Finished starting Overwatch.");
#endif
                                CreateCustomGame(owProcess.MainWindowHandle);
                                if (bmp != null)
                                {
                                    bmp.Dispose();
                                }
                            }
                            else
                            {
#if DEBUG
                                CustomGameDebug.WriteLine("Could not start Overwatch, main menu did not load.");
#endif
                                if (bmp != null)
                                {
                                    bmp.Dispose();
                                }
                                if (processInfo.CloseOverwatchProcessOnFailure)
                                {
                                    owProcess.CloseMainWindow();
                                    owProcess.Close();
                                }
                                throw new OverwatchStartFailedException("Could not start Overwatch, main menu did not load.");
                            }
                        }
#if DEBUG
                        else
                        {
                            CustomGameDebug.WriteLine("Finished starting Overwatch.");
                        }
#endif

                        return(newProcessList[i]);
                    }
                }

                Thread.Sleep(200);
            }

#if DEBUG
            CustomGameDebug.WriteLine("Error: Overwatch took too long to start.");
#endif
            RestoreVideoSettings(processInfo.OverwatchSettingsFilePath, initialSettings);
            throw new OverwatchStartFailedException("Overwatch took too long to start.");
        }
        /// <summary>
        /// Creates a new Overwatch process by logging into an account. Since this requires your username and password, I recommend using <see cref="StartOverwatch(OverwatchInfoAuto)"/> instead.
        /// </summary>
        /// <param name="processInfo">Parameters for creating the process.</param>
        /// <returns>The created Overwatch process.</returns>
        public static Process StartOverwatch(OverwatchInfoManual processInfo)
        {
            if (processInfo == null)
            {
                throw new ArgumentNullException(nameof(processInfo));
            }

            int maxWaitTime = 5000;

            if (!File.Exists(processInfo.OverwatchExecutableFilePath))
            {
                throw new FileNotFoundException(string.Format("Overwatch's executable at {0} was not found. " +
                                                              "Change OverwatchProcessInfo.OverwatchExecutableFilePath to the location of the Overwatch executable.", processInfo.OverwatchExecutableFilePath));
            }

            if (!File.Exists(processInfo.OverwatchSettingsFilePath))
            {
                throw new FileNotFoundException(string.Format("Overwatch's settings at {0} was not found. " +
                                                              "Change OverwatchProcessInfo.OverwatchSettingsFilePath to the location of Overwatch's settings.", processInfo.OverwatchSettingsFilePath));
            }

            // Set the video settings.
            var initialSettings = ChangeVideoSettings(processInfo.OverwatchSettingsFilePath, VideoSettings.Item1, VideoSettings.Item2);

            Process OWProcess = new Process();

            OWProcess.StartInfo.FileName  = processInfo.OverwatchExecutableFilePath;
            OWProcess.StartInfo.Arguments = "-Displaymode 0";
            OWProcess.Start();

            // Wait for the window to start
            WaitForVisibleProcessWindow(OWProcess);

            // Show the window
            SetupWindow(OWProcess.MainWindowHandle, processInfo.ScreenshotMethod);

            Stopwatch elapsed = new Stopwatch();

            DirectBitmap bmp = null;

            elapsed.Start();
            while (true)
            {
                Screenshot(processInfo.ScreenshotMethod, OWProcess.MainWindowHandle, ref bmp);

                if (elapsed.ElapsedMilliseconds >= maxWaitTime)
                {
                    bmp.Dispose();
                    ProcessCreateError(initialSettings, processInfo, OWProcess, bmp, new OverwatchStartFailedException("Failed to start Overwatch."));
                }

                // If the text input for the log in info is found, break out of the loop.
                if (bmp.CompareColor(407, 384, new int[] { 168, 168, 170 }, 10))
                {
                    break;
                }
                // If the log in button is yellow, there is not a connection.
                else if (bmp.CompareColor(Points.PRE_MAIN_MENU_LOGIN, Colors.CONFIRM, Fades.CONFIRM))
                {
                    ProcessCreateError(initialSettings, processInfo, OWProcess, bmp, new OverwatchStartFailedException("Could not log in; no internet connection."));
                }
                Thread.Sleep(500);
            }
            elapsed.Reset();

            Thread.Sleep(100);

            // At this point login info is ready to be inputed
            TextInput(OWProcess.MainWindowHandle, processInfo.Username);
            KeyPress(OWProcess.MainWindowHandle, Keys.Tab);
            TextInput(OWProcess.MainWindowHandle, processInfo.Password);

            // Log in
            Thread.Sleep(50);
            Screenshot(processInfo.ScreenshotMethod, OWProcess.MainWindowHandle, ref bmp);
            if (bmp.CompareColor(Points.PRE_MAIN_MENU_LOGIN, Colors.CONFIRM, Fades.CONFIRM))
            {
                KeyPress(OWProcess.MainWindowHandle, Keys.Enter);
            }
            else
            {
                ProcessCreateError(initialSettings, processInfo, OWProcess, bmp, new OverwatchStartFailedException("Could not log in with the input username or password."));
            }

            Thread.Sleep(500);

            elapsed.Start();
            while (true)
            {
                if (elapsed.ElapsedMilliseconds >= maxWaitTime)
                {
                    ProcessCreateError(initialSettings, processInfo, OWProcess, bmp, new OverwatchStartFailedException("Failed to start Overwatch."));
                }

                Screenshot(processInfo.ScreenshotMethod, OWProcess.MainWindowHandle, ref bmp);
                if (bmp.CompareColor(469, 437, Colors.CONFIRM, Fades.CONFIRM) == false)
                {
                    break;
                }
                Thread.Sleep(500);
            }
            elapsed.Reset();

            Thread.Sleep(500);

            Screenshot(processInfo.ScreenshotMethod, OWProcess.MainWindowHandle, ref bmp);
            // Check if login failed
            // s0 will equal true and s1 will equal false if the login failed. s0 and s1 will equal true if an authenticator is required.
            bool s0 = bmp.CompareColor(518, 482, Colors.CONFIRM, Fades.CONFIRM); // "Cancel" button
            bool s1 = bmp.CompareColor(605, 475, Colors.CONFIRM, Fades.CONFIRM); // "Authenticate" button.

            if (s0 && !s1)
            {
                ProcessCreateError(initialSettings, processInfo, OWProcess, bmp, new OverwatchStartFailedException("Could not log in with the input username or password."));
            }

            // Enter authenticator code if it is required
            if (s0 && s1)
            {
                if (String.IsNullOrEmpty(processInfo.Authenticator))
                {
                    ProcessCreateError(initialSettings, processInfo, OWProcess, bmp, new OverwatchStartFailedException("Authenticator is required"));
                }

                TextInput(OWProcess.MainWindowHandle, processInfo.Authenticator);
                Thread.Sleep(10);
                KeyPress(OWProcess.MainWindowHandle, Keys.Enter);
                Thread.Sleep(500);

                elapsed.Start();
                while (true)
                {
                    if (elapsed.ElapsedMilliseconds >= maxWaitTime)
                    {
                        ProcessCreateError(initialSettings, processInfo, OWProcess, bmp, new OverwatchStartFailedException("Failed to start Overwatch."));
                    }

                    Screenshot(processInfo.ScreenshotMethod, OWProcess.MainWindowHandle, ref bmp);
                    if (bmp.CompareColor(469, 437, Colors.CONFIRM, Fades.CONFIRM) == false)
                    {
                        break;
                    }
                    Thread.Sleep(500);
                }
                Thread.Sleep(500);

                Screenshot(processInfo.ScreenshotMethod, OWProcess.MainWindowHandle, ref bmp);
                if (bmp.CompareColor(518, 482, Colors.CONFIRM, Fades.CONFIRM))
                {
                    ProcessCreateError(initialSettings, processInfo, OWProcess, bmp, new OverwatchStartFailedException(string.Format("Authenticator number \"{0}\" is invalid.", processInfo.Authenticator)));
                }
            }

            if (!WaitForMainMenu(processInfo.ScreenshotMethod, OWProcess.MainWindowHandle, bmp, maxWaitTime))
            {
                ProcessCreateError(initialSettings, processInfo, OWProcess, bmp, new OverwatchStartFailedException("Failed to start Overwatch."));
            }

            if (processInfo.AutomaticallyCreateCustomGame)
            {
                CreateCustomGame(OWProcess.MainWindowHandle);
            }

            // Reset the contrast to its initial value
            RestoreVideoSettings(processInfo.OverwatchSettingsFilePath, initialSettings);

            return(OWProcess);
        }
        /// <summary>
        /// Scans for an option at the specified point.
        /// </summary>
        /// <param name="scanLocation">The location to scan at.</param>
        /// <param name="flags">The flags for scanning.</param>
        /// <param name="saveMarkupsToFolder">The location to save the markup of the scanned options. Set to null to ignore.</param>
        /// <param name="markup">The markup to scan for. Set to null to ignore.</param>
        /// <returns><para>Returns a bool determining if the option is found if <paramref name="markup"/> is not null and <paramref name="flags"/> has the <see cref="OptionScanFlags.ReturnFound"/> flag.</para>
        /// <para>Returns the location of the option if <paramref name="markup"/> is not null and <paramref name="flags"/> has the <see cref="OptionScanFlags.ReturnLocation"/> flag.</para></returns>
        public object MenuOptionScan(Point scanLocation, OptionScanFlags flags, string saveMarkupsToFolder, DirectBitmap markup)
        {
            if (saveMarkupsToFolder != null)
            {
                saveMarkupsToFolder = System.IO.Path.GetDirectoryName(saveMarkupsToFolder) + System.IO.Path.DirectorySeparatorChar;
            }

            using (cg.LockHandler.SemiInteractive)
            {
                if (scanLocation == Point.Empty)
                {
                    if (flags.HasFlag(OptionScanFlags.ReturnFound))
                    {
                        return(false);
                    }
                    else if (flags.HasFlag(OptionScanFlags.ReturnLocation))
                    {
                        return(Point.Empty);
                    }
                    else
                    {
                        return(null);
                    }
                }

                if (flags.HasFlag(OptionScanFlags.OpenMenu))
                {
                    cg.RightClick(scanLocation);
                }

                int xStart       = scanLocation.X + 14, // X position to start scanning.
                    yStart       = 0,                   // Y position to start scanning.
                    optionWidth  = 79,                  // The width of the option.
                    optionHeight = 6,                   // The height of the option.
                    yIncrement   = 1;                   // Pixel distance between options.
                int[] textcolor  = new int[] { 169, 169, 169 };
                int   fade       = 80;

                bool menuPointsDown = MenuPointsDown(scanLocation);

                if (menuPointsDown) // The menu points down.
                {
                    yStart = scanLocation.Y + 12;
                }
                else // The menu points up.
                {
                    yStart     = scanLocation.Y - 18;
                    yIncrement = -yIncrement;
                }

                cg.UpdateScreen();
                var options = new List <Tuple <Point, int> >();

                int optionIndex = 0;
                for (int y = yStart; optionHeight < y && y < cg.Capture.Height - optionHeight; y += yIncrement)
                {
                    bool oob = false;
                    while (!(oob = optionHeight > y || y > cg.Capture.Height - optionHeight) && !Capture.CompareColor(xStart, y + (menuPointsDown ? 1 : -optionHeight), textcolor, fade + 20))
                    {
                        y += yIncrement;
                    }

                    // If the y is out of range of the bitmap, stop scanning the options.
                    if (oob || !Capture.CompareColor(xStart - 8, y, new int[] { 67, 67, 68 }, 30))
                    {
                        break;
                    }

                    int percent = 0;
                    if (markup != null)
                    {
                        int success = 0;
                        int total   = 0;

                        for (int xi = 0; xi < markup.Width; xi++)
                        {
                            for (int yi = 0; yi < markup.Height; yi++)
                            {
                                total++;

                                bool bmpPixelIsBlack    = Capture.CompareColor(xStart + xi, y + yi, new int[] { 170, 170, 170 }, 80);
                                bool markupPixelIsBlack = markup.GetPixel(xi, yi) == Color.FromArgb(0, 0, 0);

                                if (bmpPixelIsBlack == markupPixelIsBlack)
                                {
                                    success++;
                                }
                            }
                        }
                        percent = (int)(Convert.ToDouble(success) / Convert.ToDouble(total) * 100);
                    }

                    // Get bitmap of option
                    if (saveMarkupsToFolder != null)
                    {
                        DirectBitmap work = Capture.Clone(xStart, y, optionWidth, optionHeight);

                        for (int xi = 0; xi < work.Width; xi++)
                        {
                            for (int yi = 0; yi < work.Height; yi++)
                            {
                                if (work.CompareColor(xi, yi, textcolor, fade))
                                {
                                    work.SetPixel(xi, yi, Color.Black);
                                }
                                else
                                {
                                    work.SetPixel(xi, yi, Color.White);
                                }
                            }
                        }

                        work.Save($@"{saveMarkupsToFolder}Option Markup-{optionIndex}.png");
                        work.Dispose();
                    }

#if DEBUG
                    if (cg.DebugMenu != null)
                    {
                        Console.WriteLine($"{optionIndex} - {percent}%");
                    }
#endif

                    options.Add(new Tuple <Point, int>(new Point(xStart, y), percent));
                    optionIndex++;
                }

                Point optionLocation = Point.Empty;

                if (markup != null && options.Count > 0)
                {
                    optionLocation = options.Where(o => o.Item2 > 75).OrderByDescending(o => o.Item2).FirstOrDefault()?.Item1 ?? Point.Empty;
                }

                if (flags.HasFlag(OptionScanFlags.Click))
                {
                    SelectMenuOption(optionLocation);
                }

                // Close the menu.
                if (flags.HasFlag(OptionScanFlags.CloseMenu) || (flags.HasFlag(OptionScanFlags.CloseIfNotFound) && optionLocation == Point.Empty))
                {
                    CloseOptionMenu();
                }

                if (flags.HasFlag(OptionScanFlags.ReturnFound))
                {
                    return(optionLocation != Point.Empty);
                }
                else if (flags.HasFlag(OptionScanFlags.ReturnLocation))
                {
                    return(optionLocation);
                }
                return(null);
            }
        }