[
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Report an issue with the patcher\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Description**\nA clear and concise description of what the bug is.\n\n**Version Information**\n* What Operating System are you running the patcher on?\n* What Unity Version are you using?\n\n**Diagnostics**\nWhat does the patcher output instead of the expected behaviour?\n\n**Arguments**\nPlease provide the exact command line arguments you are using the patcher with, e.g.:\n\n`sudo ./linux-x64/Patcher -e=/path/to/Unity -t=dark -linux`\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/incompatible-version.md",
    "content": "---\nname: Incompatible Version\nabout: Report an issue related to patcher compatibility\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Description**\nA clear and concise description of what the bug is.\n\n**Version Information**\n* What Operating System are you running the patcher on?\n* What Unity Version are you using?\n\n**Diagnostics**\nWhat does the patcher output instead of the expected behaviour?\n\n**Arguments**\nPlease provide the exact command line arguments you are using the patcher with, e.g.:\n\n`sudo ./linux-x64/Patcher -e=/path/to/Unity -t=dark -linux`\n\n**Binaries**\nIf possible, provide a link to the Unity binary that corresponds with the version and OS you're using.\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non: [push]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v1\n    - name: Setup\n      uses: actions/setup-dotnet@v1\n      with:\n        dotnet-version: 3.1.201\n    - name: Build\n      run: dotnet build ./src/Patcher.sln --configuration Release\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# DNX\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# TODO: Comment the next line if you want to checkin your web deploy settings\n# but database connection strings (with potential passwords) will be unencrypted\n#*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/packages/*\n# except build/, which is used as an MSBuild target.\n!**/packages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/packages/repositories.config\n# NuGet v3's project.json files produces more ignoreable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\nnode_modules/\norleans.codegen.cs\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# CodeRush\n.cr/\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n/Patcher/Properties/launchSettings.json\n\n# macOS\n.DS_Store\n"
  },
  {
    "path": "README.md",
    "content": "![CI Status](https://github.com/aevitas/unity-patch/workflows/CI/badge.svg)\n\nUnity Patch\n===========\n\nThis repository contains a patch for Unity that allows you to set options inaccessible from the application's menus.\n\nCurrently, the only supported option for the patch is switching between the dark and light themes in Unity.\n\nUsage\n=====\n\nWe provide binaries for Windows 10, Linux, and macOS. All compiled binaries are x64.\nSee the [release section](https://github.com/aevitas/unity-patch/releases).\nAlternatively, you can build the patch from source.\n\nRun `patcher.exe` on windows, or alternatively `Patcher` on Linux or MacOS. By default, it will locate your Unity install\nat `C:\\Program Files\\Unity\\Editor\\Unity.exe`, which is obviously wrong for both Linux and macOS, and it will set your theme to dark.\n\nYou can pass various arguments to the patcher:\n\n* `exe=` or `e=` to specify the location of the Unity executable\n* `theme=` or `t=` to set the theme, currently only `light` or `dark` are valid\n* `help` or `h` to display the options the patcher supports\n* `--windows` for Windows builds of Unity\n* `--linux` for Linux builds of Unity\n* `--mac` for MacOS builds of Unity\n* `--force` or `--f` to gently apply force\n\nDepending on your system, looking up the offsets to patch can take a couple moments.\n\nUnity Versions\n--------------\n\nThe patcher supports multiple versions of Unity. Versions can be specified by passing the `-v=` or `--version=` command line argument.\n\nFor instance, if you want to patch Unity version 2020.1 on Windows, you'd run:\n\n```\npatcher.exe --windows --version=2020.1 --t=dark\n```\n\nCurrently, the following OS and Unity version combinations are supported:\n\n|         | Windows            | MacOS              | Linux              |\n|---------|:------------------:|:------------------:|:------------------:|\n| 2020.2a |         :x:        | :white_check_mark: | :white_check_mark: |\n| 2020.1b | :white_check_mark: | :white_check_mark: | :white_check_mark: |\n| 2019.4  | :white_check_mark: | :white_check_mark: | :white_check_mark: |\n| 2019.3  | :white_check_mark: | :white_check_mark: | :white_check_mark: |\n| 2019.2  | :white_check_mark: | :white_check_mark: | :white_check_mark: |\n| 2019.1  |         :x:        | :white_check_mark: | :white_check_mark: | \n| 2018.4  | :white_check_mark: | :white_check_mark: | :white_check_mark: |\n| 2018.3  | :white_check_mark: |         :x:        |         :x:        |\n| 2018.2  | :white_check_mark: |         :x:        |         :x:        |\n\nIf you don't specify a version, the patcher will select the most recent version for your operating system.\n\nTroubleshooting\n===============\n\nTo get the highest chance of success, you should always run the patch on a clean install of Unity. If that doesn't work, you can try:\n\n* Resetting your user preferences either manually or by calling [`EditorPrefs.DeleteAll()`](https://github.com/aevitas/unity-patch/issues/17#issuecomment-592070343)\n* On MacOS Unity might be displaying a mix of Dark and Light Themes after patching. This can be resolved by restarting Unity. After restarting Unity the Theme should display correctly.\n* If you get `command not found`, try changing permissions for the file by running `chmod +x Patcher`. If running the patcher again gives you the following error `Can not patch the specified file - it is marked as read only!` then you need to check Unity to ensure you have write permissions for the `Unity.app` file as well.\n\nIssues\n======\n\nIf the patcher doesn't work, please let us know by opening an [issue](https://github.com/aevitas/unity-patch/issues), and provide as much details as you can. We provide some issue templates for common issues - please use them when applicable. They help us resolve issues faster.\n\nLinux and MacOS\n===============\n\nWhen running the patcher on Linux or MacOS, be sure to run the respective binaries for your operating system. They are located in `osx-x64` for Mac, and `linux-x64` for Linux.\n\n* Mac users should run the patcher with the `--mac` command line option\n* Linux users should run the patcher with the `--linux` command line option\n\nFor example, on Linux you would run:\n\n`sudo ./linux-x64/Patcher -e=/path/to/Unity --t=dark --linux`\n\nor on Mac:\n\n`sudo ./osx-64/Patcher -e=/Applications/Unity/Hub/Editor/<VERSION>/Unity.app/Contents/MacOS/Unity --mac --t=dark`\n"
  },
  {
    "path": "src/Patcher/BinarySearcher.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace Patcher\n{\n    /// <summary>\n    /// Original by Matthew Watson from https://stackoverflow.com/questions/37500629/find-byte-sequence-within-a-byte-array\n    /// </summary>\n    public sealed class BinarySearcher\n    {\n        readonly byte[] needle;\n        readonly int[] charTable;\n        readonly int[] offsetTable;\n\n        public BinarySearcher(byte[] needle)\n        {\n            this.needle = needle;\n            this.charTable = makeByteTable(needle);\n            this.offsetTable = makeOffsetTable(needle);\n        }\n\n        public IEnumerable<int> Search(byte[] haystack)\n        {\n            if (needle.Length == 0)\n                yield break;\n\n            for (int i = needle.Length - 1; i < haystack.Length;)\n            {\n                int j;\n\n                for (j = needle.Length - 1; needle[j] == haystack[i]; --i, --j)\n                {\n                    if (j != 0)\n                        continue;\n\n                    yield return i;\n                    i += needle.Length - 1;\n                    break;\n                }\n\n                i += Math.Max(offsetTable[needle.Length - 1 - j], charTable[haystack[i]]);\n            }\n        }\n\n        static int[] makeByteTable(byte[] needle)\n        {\n            int[] table = new int[256];\n\n            for (int i = 0; i < table.Length; ++i)\n                table[i] = needle.Length;\n\n            for (int i = 0; i < needle.Length - 1; ++i)\n                table[needle[i]] = needle.Length - 1 - i;\n\n            return table;\n        }\n\n        static int[] makeOffsetTable(byte[] needle)\n        {\n            int[] table = new int[needle.Length];\n            int lastPrefixPosition = needle.Length;\n\n            for (int i = needle.Length - 1; i >= 0; --i)\n            {\n                if (isPrefix(needle, i + 1))\n                    lastPrefixPosition = i + 1;\n\n                table[needle.Length - 1 - i] = lastPrefixPosition - i + needle.Length - 1;\n            }\n\n            for (int i = 0; i < needle.Length - 1; ++i)\n            {\n                int slen = suffixLength(needle, i);\n                table[slen] = needle.Length - 1 - i + slen;\n            }\n\n            return table;\n        }\n\n        static bool isPrefix(byte[] needle, int p)\n        {\n            for (int i = p, j = 0; i < needle.Length; ++i, ++j)\n                if (needle[i] != needle[j])\n                    return false;\n\n            return true;\n        }\n\n        static int suffixLength(byte[] needle, int p)\n        {\n            int len = 0;\n\n            for (int i = p, j = needle.Length - 1; i >= 0 && needle[i] == needle[j]; --i, --j)\n                ++len;\n\n            return len;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Patcher/ConsoleUtility.cs",
    "content": "using System;\nusing System.Collections.Generic;\n\nnamespace Patcher\n{\n    /// <summary>\n    /// Utility methods for interacting with the console.\n    /// </summary>\n    public static class ConsoleUtility\n    {\n        /// <summary>\n        /// Displays a selection menu in the console to the user which can be navigated using the arrow keys and enter\n        /// to select the desired option from the list.\n        /// </summary>\n        /// <param name=\"options\">The list of options from which the user can choose</param>\n        /// <param name=\"optionLine\">\n        /// A function taking one of the options an returning a string representation of how it should be displayed to the user\n        /// </param>\n        /// <typeparam name=\"T\">The type of a single option</typeparam>\n        /// <returns>The selected option from the list</returns>\n        public static T ShowSelectionMenu<T>(List<T> options, Func<T, string> optionLine)\n        {\n            int selected = 0;\n            bool done = false;\n\n            while (!done)\n            {\n                for (int i = 0; i < options.Count; i++)\n                {\n                    if (i == selected)\n                    {\n                        Console.ForegroundColor = ConsoleColor.Cyan;\n                        Console.Write(\"> \");\n                    }\n                    else\n                    {\n                        Console.Write(\"  \");\n                    }\n                    Console.WriteLine(optionLine(options[i]));\n                    Console.ResetColor();\n                }\n\n                switch (Console.ReadKey(true).Key)\n                {\n                    case ConsoleKey.UpArrow:\n                        selected = Math.Max(0, selected - 1);\n                        break;\n                    case ConsoleKey.DownArrow:\n                        selected = Math.Min(options.Count - 1, selected + 1);\n                        break;\n                    case ConsoleKey.Spacebar:\n                    case ConsoleKey.Enter:\n                        done = true;\n                        break;\n                }\n\n                if (!done)\n                    Console.CursorTop -= options.Count;\n            }\n            \n            return options[selected];\n        }\n    }\n}\n"
  },
  {
    "path": "src/Patcher/OperatingSystem.cs",
    "content": "namespace Patcher\n{\n    public enum OperatingSystem\n    {\n        Unknown,\n        Windows,\n        MacOS,\n        Linux\n    }\n}\n"
  },
  {
    "path": "src/Patcher/Options.cs",
    "content": "﻿//\n// Options.cs\n//\n// Authors:\n//  Jonathan Pryor <jpryor@novell.com>\n//\n// Copyright (C) 2008 Novell (http://www.novell.com)\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n// \n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n// \n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n//\n\n// Compile With:\n//   gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll\n//   gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll\n//\n// The LINQ version just changes the implementation of\n// OptionSet.Parse(IEnumerable<string>), and confers no semantic changes.\n\n//\n// A Getopt::Long-inspired option parsing library for C#.\n//\n// NDesk.Options.OptionSet is built upon a key/value table, where the\n// key is a option format string and the value is a delegate that is \n// invoked when the format string is matched.\n//\n// Option format strings:\n//  Regex-like BNF Grammar: \n//    name: .+\n//    type: [=:]\n//    sep: ( [^{}]+ | '{' .+ '}' )?\n//    aliases: ( name type sep ) ( '|' name type sep )*\n// \n// Each '|'-delimited name is an alias for the associated action.  If the\n// format string ends in a '=', it has a required value.  If the format\n// string ends in a ':', it has an optional value.  If neither '=' or ':'\n// is present, no value is supported.  `=' or `:' need only be defined on one\n// alias, but if they are provided on more than one they must be consistent.\n//\n// Each alias portion may also end with a \"key/value separator\", which is used\n// to split option values if the option accepts > 1 value.  If not specified,\n// it defaults to '=' and ':'.  If specified, it can be any character except\n// '{' and '}' OR the *string* between '{' and '}'.  If no separator should be\n// used (i.e. the separate values should be distinct arguments), then \"{}\"\n// should be used as the separator.\n//\n// Options are extracted either from the current option by looking for\n// the option name followed by an '=' or ':', or is taken from the\n// following option IFF:\n//  - The current option does not contain a '=' or a ':'\n//  - The current option requires a value (i.e. not a Option type of ':')\n//\n// The `name' used in the option format string does NOT include any leading\n// option indicator, such as '-', '--', or '/'.  All three of these are\n// permitted/required on any named option.\n//\n// Option bundling is permitted so long as:\n//   - '-' is used to start the option group\n//   - all of the bundled options are a single character\n//   - at most one of the bundled options accepts a value, and the value\n//     provided starts from the next character to the end of the string.\n//\n// This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value'\n// as '-Dname=value'.\n//\n// Option processing is disabled by specifying \"--\".  All options after \"--\"\n// are returned by OptionSet.Parse() unchanged and unprocessed.\n//\n// Unprocessed options are returned from OptionSet.Parse().\n//\n// Examples:\n//  int verbose = 0;\n//  OptionSet p = new OptionSet ()\n//    .Add (\"v\", v => ++verbose)\n//    .Add (\"name=|value=\", v => Console.WriteLine (v));\n//  p.Parse (new string[]{\"-v\", \"--v\", \"/v\", \"-name=A\", \"/name\", \"B\", \"extra\"});\n//\n// The above would parse the argument string array, and would invoke the\n// lambda expression three times, setting `verbose' to 3 when complete.  \n// It would also print out \"A\" and \"B\" to standard output.\n// The returned array would contain the string \"extra\".\n//\n// C# 3.0 collection initializers are supported and encouraged:\n//  var p = new OptionSet () {\n//    { \"h|?|help\", v => ShowHelp () },\n//  };\n//\n// System.ComponentModel.TypeConverter is also supported, allowing the use of\n// custom data types in the callback type; TypeConverter.ConvertFromString()\n// is used to convert the value option to an instance of the specified\n// type:\n//\n//  var p = new OptionSet () {\n//    { \"foo=\", (Foo f) => Console.WriteLine (f.ToString ()) },\n//  };\n//\n// Random other tidbits:\n//  - Boolean options (those w/o '=' or ':' in the option format string)\n//    are explicitly enabled if they are followed with '+', and explicitly\n//    disabled if they are followed with '-':\n//      string a = null;\n//      var p = new OptionSet () {\n//        { \"a\", s => a = s },\n//      };\n//      p.Parse (new string[]{\"-a\"});   // sets v != null\n//      p.Parse (new string[]{\"-a+\"});  // sets v != null\n//      p.Parse (new string[]{\"-a-\"});  // sets v == null\n//\n\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.ComponentModel;\nusing System.Globalization;\nusing System.IO;\nusing System.Runtime.Serialization;\nusing System.Security.Permissions;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\n#if LINQ\nusing System.Linq;\n#endif\n\n#if TEST\nusing NDesk.Options;\n#endif\n\nnamespace NDesk.Options\n{\n\n    public class OptionValueCollection : IList, IList<string>\n    {\n\n        List<string> values = new List<string>();\n        OptionContext c;\n\n        internal OptionValueCollection(OptionContext c)\n        {\n            this.c = c;\n        }\n\n        #region ICollection\n        void ICollection.CopyTo(Array array, int index) { (values as ICollection).CopyTo(array, index); }\n        bool ICollection.IsSynchronized { get { return (values as ICollection).IsSynchronized; } }\n        object ICollection.SyncRoot { get { return (values as ICollection).SyncRoot; } }\n        #endregion\n\n        #region ICollection<T>\n        public void Add(string item) { values.Add(item); }\n        public void Clear() { values.Clear(); }\n        public bool Contains(string item) { return values.Contains(item); }\n        public void CopyTo(string[] array, int arrayIndex) { values.CopyTo(array, arrayIndex); }\n        public bool Remove(string item) { return values.Remove(item); }\n        public int Count { get { return values.Count; } }\n        public bool IsReadOnly { get { return false; } }\n        #endregion\n\n        #region IEnumerable\n        IEnumerator IEnumerable.GetEnumerator() { return values.GetEnumerator(); }\n        #endregion\n\n        #region IEnumerable<T>\n        public IEnumerator<string> GetEnumerator() { return values.GetEnumerator(); }\n        #endregion\n\n        #region IList\n        int IList.Add(object value) { return (values as IList).Add(value); }\n        bool IList.Contains(object value) { return (values as IList).Contains(value); }\n        int IList.IndexOf(object value) { return (values as IList).IndexOf(value); }\n        void IList.Insert(int index, object value) { (values as IList).Insert(index, value); }\n        void IList.Remove(object value) { (values as IList).Remove(value); }\n        void IList.RemoveAt(int index) { (values as IList).RemoveAt(index); }\n        bool IList.IsFixedSize { get { return false; } }\n        object IList.this[int index] { get { return this[index]; } set { (values as IList)[index] = value; } }\n        #endregion\n\n        #region IList<T>\n        public int IndexOf(string item) { return values.IndexOf(item); }\n        public void Insert(int index, string item) { values.Insert(index, item); }\n        public void RemoveAt(int index) { values.RemoveAt(index); }\n\n        private void AssertValid(int index)\n        {\n            if (c.Option == null)\n                throw new InvalidOperationException(\"OptionContext.Option is null.\");\n            if (index >= c.Option.MaxValueCount)\n                throw new ArgumentOutOfRangeException(\"index\");\n            if (c.Option.OptionValueType == OptionValueType.Required &&\n                    index >= values.Count)\n                throw new OptionException(string.Format(\n                            c.OptionSet.MessageLocalizer(\"Missing required value for option '{0}'.\"), c.OptionName),\n                        c.OptionName);\n        }\n\n        public string this[int index]\n        {\n            get\n            {\n                AssertValid(index);\n                return index >= values.Count ? null : values[index];\n            }\n            set\n            {\n                values[index] = value;\n            }\n        }\n        #endregion\n\n        public List<string> ToList()\n        {\n            return new List<string>(values);\n        }\n\n        public string[] ToArray()\n        {\n            return values.ToArray();\n        }\n\n        public override string ToString()\n        {\n            return string.Join(\", \", values.ToArray());\n        }\n    }\n\n    public class OptionContext\n    {\n        private Option option;\n        private string name;\n        private int index;\n        private readonly OptionSet set;\n        private readonly OptionValueCollection c;\n\n        public OptionContext(OptionSet set)\n        {\n            this.set = set;\n            this.c = new OptionValueCollection(this);\n        }\n\n        public Option Option\n        {\n            get { return option; }\n            set { option = value; }\n        }\n\n        public string OptionName\n        {\n            get { return name; }\n            set { name = value; }\n        }\n\n        public int OptionIndex\n        {\n            get { return index; }\n            set { index = value; }\n        }\n\n        public OptionSet OptionSet\n        {\n            get { return set; }\n        }\n\n        public OptionValueCollection OptionValues\n        {\n            get { return c; }\n        }\n    }\n\n    public enum OptionValueType\n    {\n        None,\n        Optional,\n        Required,\n    }\n\n    public abstract class Option\n    {\n        readonly string prototype, description;\n        string[] names;\n        readonly OptionValueType type;\n        readonly int count;\n        string[] separators;\n\n        protected Option(string prototype, string description)\n            : this(prototype, description, 1)\n        {\n        }\n\n        protected Option(string prototype, string description, int maxValueCount)\n        {\n            if (prototype == null)\n                throw new ArgumentNullException(\"prototype\");\n            if (prototype.Length == 0)\n                throw new ArgumentException(\"Cannot be the empty string.\", \"prototype\");\n            if (maxValueCount < 0)\n                throw new ArgumentOutOfRangeException(\"maxValueCount\");\n\n            this.prototype = prototype;\n            this.names = prototype.Split('|');\n            this.description = description;\n            this.count = maxValueCount;\n            this.type = ParsePrototype();\n\n            if (this.count == 0 && type != OptionValueType.None)\n                throw new ArgumentException(\n                        \"Cannot provide maxValueCount of 0 for OptionValueType.Required or \" +\n                            \"OptionValueType.Optional.\",\n                        \"maxValueCount\");\n            if (this.type == OptionValueType.None && maxValueCount > 1)\n                throw new ArgumentException(\n                        string.Format(\"Cannot provide maxValueCount of {0} for OptionValueType.None.\", maxValueCount),\n                        \"maxValueCount\");\n            if (Array.IndexOf(names, \"<>\") >= 0 &&\n                    ((names.Length == 1 && this.type != OptionValueType.None) ||\n                     (names.Length > 1 && this.MaxValueCount > 1)))\n                throw new ArgumentException(\n                        \"The default option handler '<>' cannot require values.\",\n                        \"prototype\");\n        }\n\n        public string Prototype { get { return prototype; } }\n        public string Description { get { return description; } }\n        public OptionValueType OptionValueType { get { return type; } }\n        public int MaxValueCount { get { return count; } }\n\n        public string[] GetNames()\n        {\n            return (string[])names.Clone();\n        }\n\n        public string[] GetValueSeparators()\n        {\n            if (separators == null)\n                return new string[0];\n            return (string[])separators.Clone();\n        }\n\n        protected static T Parse<T>(string value, OptionContext c)\n        {\n            TypeConverter conv = TypeDescriptor.GetConverter(typeof(T));\n            T t = default;\n            try\n            {\n                if (value != null)\n                    t = (T)conv.ConvertFromString(value);\n            }\n            catch (Exception e)\n            {\n                throw new OptionException(\n                        string.Format(\n                            c.OptionSet.MessageLocalizer(\"Could not convert string `{0}' to type {1} for option `{2}'.\"),\n                            value, typeof(T).Name, c.OptionName),\n                        c.OptionName, e);\n            }\n            return t;\n        }\n\n        internal string[] Names { get { return names; } }\n        internal string[] ValueSeparators { get { return separators; } }\n\n        static readonly char[] NameTerminator = new char[] { '=', ':' };\n\n        private OptionValueType ParsePrototype()\n        {\n            char type = '\\0';\n            List<string> seps = new List<string>();\n            for (int i = 0; i < names.Length; ++i)\n            {\n                string name = names[i];\n                if (name.Length == 0)\n                    throw new ArgumentException(\"Empty option names are not supported.\", \"prototype\");\n\n                int end = name.IndexOfAny(NameTerminator);\n                if (end == -1)\n                    continue;\n                names[i] = name.Substring(0, end);\n                if (type == '\\0' || type == name[end])\n                    type = name[end];\n                else\n                    throw new ArgumentException(\n                            string.Format(\"Conflicting option types: '{0}' vs. '{1}'.\", type, name[end]),\n                            \"prototype\");\n                AddSeparators(name, end, seps);\n            }\n\n            if (type == '\\0')\n                return OptionValueType.None;\n\n            if (count <= 1 && seps.Count != 0)\n                throw new ArgumentException(\n                        string.Format(\"Cannot provide key/value separators for Options taking {0} value(s).\", count),\n                        \"prototype\");\n            if (count > 1)\n            {\n                if (seps.Count == 0)\n                    this.separators = new string[] { \":\", \"=\" };\n                else if (seps.Count == 1 && seps[0].Length == 0)\n                    this.separators = null;\n                else\n                    this.separators = seps.ToArray();\n            }\n\n            return type == '=' ? OptionValueType.Required : OptionValueType.Optional;\n        }\n\n        private static void AddSeparators(string name, int end, ICollection<string> seps)\n        {\n            int start = -1;\n            for (int i = end + 1; i < name.Length; ++i)\n            {\n                switch (name[i])\n                {\n                    case '{':\n                        if (start != -1)\n                            throw new ArgumentException(\n                                    string.Format(\"Ill-formed name/value separator found in \\\"{0}\\\".\", name),\n                                    \"prototype\");\n                        start = i + 1;\n                        break;\n                    case '}':\n                        if (start == -1)\n                            throw new ArgumentException(\n                                    string.Format(\"Ill-formed name/value separator found in \\\"{0}\\\".\", name),\n                                    \"prototype\");\n                        seps.Add(name.Substring(start, i - start));\n                        start = -1;\n                        break;\n                    default:\n                        if (start == -1)\n                            seps.Add(name[i].ToString());\n                        break;\n                }\n            }\n            if (start != -1)\n                throw new ArgumentException(\n                        string.Format(\"Ill-formed name/value separator found in \\\"{0}\\\".\", name),\n                        \"prototype\");\n        }\n\n        public void Invoke(OptionContext c)\n        {\n            OnParseComplete(c);\n            c.OptionName = null;\n            c.Option = null;\n            c.OptionValues.Clear();\n        }\n\n        protected abstract void OnParseComplete(OptionContext c);\n\n        public override string ToString()\n        {\n            return Prototype;\n        }\n    }\n\n    [Serializable]\n    public class OptionException : Exception\n    {\n        private readonly string option;\n\n        public OptionException()\n        {\n        }\n\n        public OptionException(string message, string optionName)\n            : base(message)\n        {\n            this.option = optionName;\n        }\n\n        public OptionException(string message, string optionName, Exception innerException)\n            : base(message, innerException)\n        {\n            this.option = optionName;\n        }\n\n        protected OptionException(SerializationInfo info, StreamingContext context)\n            : base(info, context)\n        {\n            this.option = info.GetString(\"OptionName\");\n        }\n\n        public string OptionName\n        {\n            get { return this.option; }\n        }\n\n        [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]\n        public override void GetObjectData(SerializationInfo info, StreamingContext context)\n        {\n            base.GetObjectData(info, context);\n            info.AddValue(\"OptionName\", option);\n        }\n    }\n\n    public delegate void OptionAction<TKey, TValue>(TKey key, TValue value);\n\n    public class OptionSet : KeyedCollection<string, Option>\n    {\n        public OptionSet()\n            : this(delegate (string f) { return f; })\n        {\n        }\n\n        public OptionSet(Converter<string, string> localizer)\n        {\n            this.localizer = localizer;\n        }\n\n        readonly Converter<string, string> localizer;\n\n        public Converter<string, string> MessageLocalizer\n        {\n            get { return localizer; }\n        }\n\n        protected override string GetKeyForItem(Option item)\n        {\n            if (item == null)\n                throw new ArgumentNullException(\"option\");\n            if (item.Names != null && item.Names.Length > 0)\n                return item.Names[0];\n            // This should never happen, as it's invalid for Option to be\n            // constructed w/o any names.\n            throw new InvalidOperationException(\"Option has no names!\");\n        }\n\n        [Obsolete(\"Use KeyedCollection.this[string]\")]\n        protected Option GetOptionForName(string option)\n        {\n            if (option == null)\n                throw new ArgumentNullException(\"option\");\n            try\n            {\n                return base[option];\n            }\n            catch (KeyNotFoundException)\n            {\n                return null;\n            }\n        }\n\n        protected override void InsertItem(int index, Option item)\n        {\n            base.InsertItem(index, item);\n            AddImpl(item);\n        }\n\n        protected override void RemoveItem(int index)\n        {\n            base.RemoveItem(index);\n            Option p = Items[index];\n            // KeyedCollection.RemoveItem() handles the 0th item\n            for (int i = 1; i < p.Names.Length; ++i)\n            {\n                Dictionary.Remove(p.Names[i]);\n            }\n        }\n\n        protected override void SetItem(int index, Option item)\n        {\n            base.SetItem(index, item);\n            RemoveItem(index);\n            AddImpl(item);\n        }\n\n        private void AddImpl(Option option)\n        {\n            if (option == null)\n                throw new ArgumentNullException(\"option\");\n            List<string> added = new List<string>(option.Names.Length);\n            try\n            {\n                // KeyedCollection.InsertItem/SetItem handle the 0th name.\n                for (int i = 1; i < option.Names.Length; ++i)\n                {\n                    Dictionary.Add(option.Names[i], option);\n                    added.Add(option.Names[i]);\n                }\n            }\n            catch (Exception)\n            {\n                foreach (string name in added)\n                    Dictionary.Remove(name);\n                throw;\n            }\n        }\n\n        public new OptionSet Add(Option option)\n        {\n            base.Add(option);\n            return this;\n        }\n\n        sealed class ActionOption : Option\n        {\n            readonly Action<OptionValueCollection> action;\n\n            public ActionOption(string prototype, string description, int count, Action<OptionValueCollection> action)\n                : base(prototype, description, count)\n            {\n                this.action = action ?? throw new ArgumentNullException(\"action\");\n            }\n\n            protected override void OnParseComplete(OptionContext c)\n            {\n                action(c.OptionValues);\n            }\n        }\n\n        public OptionSet Add(string prototype, Action<string> action)\n        {\n            return Add(prototype, null, action);\n        }\n\n        public OptionSet Add(string prototype, string description, Action<string> action)\n        {\n            if (action == null)\n                throw new ArgumentNullException(\"action\");\n            Option p = new ActionOption(prototype, description, 1,\n                    delegate (OptionValueCollection v) { action(v[0]); });\n            base.Add(p);\n            return this;\n        }\n\n        public OptionSet Add(string prototype, OptionAction<string, string> action)\n        {\n            return Add(prototype, null, action);\n        }\n\n        public OptionSet Add(string prototype, string description, OptionAction<string, string> action)\n        {\n            if (action == null)\n                throw new ArgumentNullException(\"action\");\n            Option p = new ActionOption(prototype, description, 2,\n                    delegate (OptionValueCollection v) { action(v[0], v[1]); });\n            base.Add(p);\n            return this;\n        }\n\n        sealed class ActionOption<T> : Option\n        {\n            readonly Action<T> action;\n\n            public ActionOption(string prototype, string description, Action<T> action)\n                : base(prototype, description, 1)\n            {\n                this.action = action ?? throw new ArgumentNullException(\"action\");\n            }\n\n            protected override void OnParseComplete(OptionContext c)\n            {\n                action(Parse<T>(c.OptionValues[0], c));\n            }\n        }\n\n        sealed class ActionOption<TKey, TValue> : Option\n        {\n            readonly OptionAction<TKey, TValue> action;\n\n            public ActionOption(string prototype, string description, OptionAction<TKey, TValue> action)\n                : base(prototype, description, 2)\n            {\n                this.action = action ?? throw new ArgumentNullException(\"action\");\n            }\n\n            protected override void OnParseComplete(OptionContext c)\n            {\n                action(\n                        Parse<TKey>(c.OptionValues[0], c),\n                        Parse<TValue>(c.OptionValues[1], c));\n            }\n        }\n\n        public OptionSet Add<T>(string prototype, Action<T> action)\n        {\n            return Add(prototype, null, action);\n        }\n\n        public OptionSet Add<T>(string prototype, string description, Action<T> action)\n        {\n            return Add(new ActionOption<T>(prototype, description, action));\n        }\n\n        public OptionSet Add<TKey, TValue>(string prototype, OptionAction<TKey, TValue> action)\n        {\n            return Add(prototype, null, action);\n        }\n\n        public OptionSet Add<TKey, TValue>(string prototype, string description, OptionAction<TKey, TValue> action)\n        {\n            return Add(new ActionOption<TKey, TValue>(prototype, description, action));\n        }\n\n        protected virtual OptionContext CreateOptionContext()\n        {\n            return new OptionContext(this);\n        }\n\n#if LINQ\n\t\tpublic List<string> Parse (IEnumerable<string> arguments)\n\t\t{\n\t\t\tbool process = true;\n\t\t\tOptionContext c = CreateOptionContext ();\n\t\t\tc.OptionIndex = -1;\n\t\t\tvar def = GetOptionForName (\"<>\");\n\t\t\tvar unprocessed = \n\t\t\t\tfrom argument in arguments\n\t\t\t\twhere ++c.OptionIndex >= 0 && (process || def != null)\n\t\t\t\t\t? process\n\t\t\t\t\t\t? argument == \"--\" \n\t\t\t\t\t\t\t? (process = false)\n\t\t\t\t\t\t\t: !Parse (argument, c)\n\t\t\t\t\t\t\t\t? def != null \n\t\t\t\t\t\t\t\t\t? Unprocessed (null, def, c, argument) \n\t\t\t\t\t\t\t\t\t: true\n\t\t\t\t\t\t\t\t: false\n\t\t\t\t\t\t: def != null \n\t\t\t\t\t\t\t? Unprocessed (null, def, c, argument)\n\t\t\t\t\t\t\t: true\n\t\t\t\t\t: true\n\t\t\t\tselect argument;\n\t\t\tList<string> r = unprocessed.ToList ();\n\t\t\tif (c.Option != null)\n\t\t\t\tc.Option.Invoke (c);\n\t\t\treturn r;\n\t\t}\n#else\n        public List<string> Parse(IEnumerable<string> arguments)\n        {\n            OptionContext c = CreateOptionContext();\n            c.OptionIndex = -1;\n            bool process = true;\n            List<string> unprocessed = new List<string>();\n            Option def = Contains(\"<>\") ? this[\"<>\"] : null;\n            foreach (string argument in arguments)\n            {\n                ++c.OptionIndex;\n                if (argument == \"--\")\n                {\n                    process = false;\n                    continue;\n                }\n                if (!process)\n                {\n                    Unprocessed(unprocessed, def, c, argument);\n                    continue;\n                }\n                if (!Parse(argument, c))\n                    Unprocessed(unprocessed, def, c, argument);\n            }\n            if (c.Option != null)\n                c.Option.Invoke(c);\n            return unprocessed;\n        }\n#endif\n\n        private static bool Unprocessed(ICollection<string> extra, Option def, OptionContext c, string argument)\n        {\n            if (def == null)\n            {\n                extra.Add(argument);\n                return false;\n            }\n            c.OptionValues.Add(argument);\n            c.Option = def;\n            c.Option.Invoke(c);\n            return false;\n        }\n\n        private readonly Regex ValueOption = new Regex(\n            @\"^(?<flag>--|-|/)(?<name>[^:=]+)((?<sep>[:=])(?<value>.*))?$\");\n\n        protected bool GetOptionParts(string argument, out string flag, out string name, out string sep, out string value)\n        {\n            if (argument == null)\n                throw new ArgumentNullException(\"argument\");\n\n            flag = name = sep = value = null;\n            Match m = ValueOption.Match(argument);\n            if (!m.Success)\n            {\n                return false;\n            }\n            flag = m.Groups[\"flag\"].Value;\n            name = m.Groups[\"name\"].Value;\n            if (m.Groups[\"sep\"].Success && m.Groups[\"value\"].Success)\n            {\n                sep = m.Groups[\"sep\"].Value;\n                value = m.Groups[\"value\"].Value;\n            }\n            return true;\n        }\n\n        protected virtual bool Parse(string argument, OptionContext c)\n        {\n            if (c.Option != null)\n            {\n                ParseValue(argument, c);\n                return true;\n            }\n\n            if (!GetOptionParts(argument, out string f, out string n, out string s, out string v))\n                return false;\n\n            Option p;\n            if (Contains(n))\n            {\n                p = this[n];\n                c.OptionName = f + n;\n                c.Option = p;\n                switch (p.OptionValueType)\n                {\n                    case OptionValueType.None:\n                        c.OptionValues.Add(n);\n                        c.Option.Invoke(c);\n                        break;\n                    case OptionValueType.Optional:\n                    case OptionValueType.Required:\n                        ParseValue(v, c);\n                        break;\n                }\n                return true;\n            }\n            // no match; is it a bool option?\n            if (ParseBool(argument, n, c))\n                return true;\n            // is it a bundled option?\n            if (ParseBundledValue(f, string.Concat(n + s + v), c))\n                return true;\n\n            return false;\n        }\n\n        private void ParseValue(string option, OptionContext c)\n        {\n            if (option != null)\n                foreach (string o in c.Option.ValueSeparators != null\n                        ? option.Split(c.Option.ValueSeparators, StringSplitOptions.None)\n                        : new string[] { option })\n                {\n                    c.OptionValues.Add(o);\n                }\n            if (c.OptionValues.Count == c.Option.MaxValueCount ||\n                    c.Option.OptionValueType == OptionValueType.Optional)\n                c.Option.Invoke(c);\n            else if (c.OptionValues.Count > c.Option.MaxValueCount)\n            {\n                throw new OptionException(localizer(string.Format(\n                                \"Error: Found {0} option values when expecting {1}.\",\n                                c.OptionValues.Count, c.Option.MaxValueCount)),\n                        c.OptionName);\n            }\n        }\n\n        private bool ParseBool(string option, string n, OptionContext c)\n        {\n            Option p;\n            string rn;\n            if (n.Length >= 1 && (n[n.Length - 1] == '+' || n[n.Length - 1] == '-') &&\n                    Contains((rn = n.Substring(0, n.Length - 1))))\n            {\n                p = this[rn];\n                string v = n[n.Length - 1] == '+' ? option : null;\n                c.OptionName = option;\n                c.Option = p;\n                c.OptionValues.Add(v);\n                p.Invoke(c);\n                return true;\n            }\n            return false;\n        }\n\n        private bool ParseBundledValue(string f, string n, OptionContext c)\n        {\n            if (f != \"-\")\n                return false;\n            for (int i = 0; i < n.Length; ++i)\n            {\n                Option p;\n                string opt = f + n[i].ToString();\n                string rn = n[i].ToString();\n                if (!Contains(rn))\n                {\n                    if (i == 0)\n                        return false;\n                    throw new OptionException(string.Format(localizer(\n                                    \"Cannot bundle unregistered option '{0}'.\"), opt), opt);\n                }\n                p = this[rn];\n                switch (p.OptionValueType)\n                {\n                    case OptionValueType.None:\n                        Invoke(c, opt, n, p);\n                        break;\n                    case OptionValueType.Optional:\n                    case OptionValueType.Required:\n                        {\n                            string v = n.Substring(i + 1);\n                            c.Option = p;\n                            c.OptionName = opt;\n                            ParseValue(v.Length != 0 ? v : null, c);\n                            return true;\n                        }\n                    default:\n                        throw new InvalidOperationException(\"Unknown OptionValueType: \" + p.OptionValueType);\n                }\n            }\n            return true;\n        }\n\n        private static void Invoke(OptionContext c, string name, string value, Option option)\n        {\n            c.OptionName = name;\n            c.Option = option;\n            c.OptionValues.Add(value);\n            option.Invoke(c);\n        }\n\n        private const int OptionWidth = 29;\n\n        public void WriteOptionDescriptions(TextWriter o)\n        {\n            foreach (Option p in this)\n            {\n                int written = 0;\n                if (!WriteOptionPrototype(o, p, ref written))\n                    continue;\n\n                if (written < OptionWidth)\n                    o.Write(new string(' ', OptionWidth - written));\n                else\n                {\n                    o.WriteLine();\n                    o.Write(new string(' ', OptionWidth));\n                }\n\n                List<string> lines = GetLines(localizer(GetDescription(p.Description)));\n                o.WriteLine(lines[0]);\n                string prefix = new string(' ', OptionWidth + 2);\n                for (int i = 1; i < lines.Count; ++i)\n                {\n                    o.Write(prefix);\n                    o.WriteLine(lines[i]);\n                }\n            }\n        }\n\n        bool WriteOptionPrototype(TextWriter o, Option p, ref int written)\n        {\n            string[] names = p.Names;\n\n            int i = GetNextOptionIndex(names, 0);\n            if (i == names.Length)\n                return false;\n\n            if (names[i].Length == 1)\n            {\n                Write(o, ref written, \"  -\");\n                Write(o, ref written, names[0]);\n            }\n            else\n            {\n                Write(o, ref written, \"      --\");\n                Write(o, ref written, names[0]);\n            }\n\n            for (i = GetNextOptionIndex(names, i + 1);\n                    i < names.Length; i = GetNextOptionIndex(names, i + 1))\n            {\n                Write(o, ref written, \", \");\n                Write(o, ref written, names[i].Length == 1 ? \"-\" : \"--\");\n                Write(o, ref written, names[i]);\n            }\n\n            if (p.OptionValueType == OptionValueType.Optional ||\n                    p.OptionValueType == OptionValueType.Required)\n            {\n                if (p.OptionValueType == OptionValueType.Optional)\n                {\n                    Write(o, ref written, localizer(\"[\"));\n                }\n                Write(o, ref written, localizer(\"=\" + GetArgumentName(0, p.MaxValueCount, p.Description)));\n                string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0\n                    ? p.ValueSeparators[0]\n                    : \" \";\n                for (int c = 1; c < p.MaxValueCount; ++c)\n                {\n                    Write(o, ref written, localizer(sep + GetArgumentName(c, p.MaxValueCount, p.Description)));\n                }\n                if (p.OptionValueType == OptionValueType.Optional)\n                {\n                    Write(o, ref written, localizer(\"]\"));\n                }\n            }\n            return true;\n        }\n\n        static int GetNextOptionIndex(string[] names, int i)\n        {\n            while (i < names.Length && names[i] == \"<>\")\n            {\n                ++i;\n            }\n            return i;\n        }\n\n        static void Write(TextWriter o, ref int n, string s)\n        {\n            n += s.Length;\n            o.Write(s);\n        }\n\n        private static string GetArgumentName(int index, int maxIndex, string description)\n        {\n            if (description == null)\n                return maxIndex == 1 ? \"VALUE\" : \"VALUE\" + (index + 1);\n            string[] nameStart;\n            if (maxIndex == 1)\n                nameStart = new string[] { \"{0:\", \"{\" };\n            else\n                nameStart = new string[] { \"{\" + index + \":\" };\n            for (int i = 0; i < nameStart.Length; ++i)\n            {\n                int start, j = 0;\n                do\n                {\n                    start = description.IndexOf(nameStart[i], j);\n                } while (start >= 0 && j != 0 ? description[j++ - 1] == '{' : false);\n                if (start == -1)\n                    continue;\n                int end = description.IndexOf(\"}\", start);\n                if (end == -1)\n                    continue;\n                return description.Substring(start + nameStart[i].Length, end - start - nameStart[i].Length);\n            }\n            return maxIndex == 1 ? \"VALUE\" : \"VALUE\" + (index + 1);\n        }\n\n        private static string GetDescription(string description)\n        {\n            if (description == null)\n                return string.Empty;\n            StringBuilder sb = new StringBuilder(description.Length);\n            int start = -1;\n            for (int i = 0; i < description.Length; ++i)\n            {\n                switch (description[i])\n                {\n                    case '{':\n                        if (i == start)\n                        {\n                            sb.Append('{');\n                            start = -1;\n                        }\n                        else if (start < 0)\n                            start = i + 1;\n                        break;\n                    case '}':\n                        if (start < 0)\n                        {\n                            if ((i + 1) == description.Length || description[i + 1] != '}')\n                                throw new InvalidOperationException(\"Invalid option description: \" + description);\n                            ++i;\n                            sb.Append(\"}\");\n                        }\n                        else\n                        {\n                            sb.Append(description.Substring(start, i - start));\n                            start = -1;\n                        }\n                        break;\n                    case ':':\n                        if (start < 0)\n                            goto default;\n                        start = i + 1;\n                        break;\n                    default:\n                        if (start < 0)\n                            sb.Append(description[i]);\n                        break;\n                }\n            }\n            return sb.ToString();\n        }\n\n        private static List<string> GetLines(string description)\n        {\n            List<string> lines = new List<string>();\n            if (string.IsNullOrEmpty(description))\n            {\n                lines.Add(string.Empty);\n                return lines;\n            }\n            int length = 80 - OptionWidth - 2;\n            int start = 0, end;\n            do\n            {\n                end = GetLineEnd(start, length, description);\n                bool cont = false;\n                if (end < description.Length)\n                {\n                    char c = description[end];\n                    if (c == '-' || (char.IsWhiteSpace(c) && c != '\\n'))\n                        ++end;\n                    else if (c != '\\n')\n                    {\n                        cont = true;\n                        --end;\n                    }\n                }\n                lines.Add(description.Substring(start, end - start));\n                if (cont)\n                {\n                    lines[lines.Count - 1] += \"-\";\n                }\n                start = end;\n                if (start < description.Length && description[start] == '\\n')\n                    ++start;\n            } while (end < description.Length);\n            return lines;\n        }\n\n        private static int GetLineEnd(int start, int length, string description)\n        {\n            int end = Math.Min(start + length, description.Length);\n            int sep = -1;\n            for (int i = start; i < end; ++i)\n            {\n                switch (description[i])\n                {\n                    case ' ':\n                    case '\\t':\n                    case '\\v':\n                    case '-':\n                    case ',':\n                    case '.':\n                    case ';':\n                        sep = i;\n                        break;\n                    case '\\n':\n                        return i;\n                }\n            }\n            if (sep == -1 || end == description.Length)\n                return end;\n            return sep;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Patcher/PatchInfo.cs",
    "content": "namespace Patcher\n{\n    public class PatchInfo\n    {\n        public string Version { get; set; }\n\n        public byte[] DarkPattern { get; set; }\n\n        public byte[] LightPattern { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Patcher/Patcher.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>netcoreapp3.1</TargetFramework>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\PublishProfiles\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Patcher/Patches.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Patcher\n{\n    public static class Patches\n    {\n        private static readonly List<PatchInfo> WindowsPatches = new List<PatchInfo>\n        {\n            new PatchInfo\n            {\n                Version = \"2018.2\",\n                LightPattern = new byte[] {0x75, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30},\n                DarkPattern = new byte[] {0x74, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30}\n            },\n            new PatchInfo\n            {\n                Version = \"2018.3\",\n                LightPattern = new byte[] {0x75, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30},\n                DarkPattern = new byte[] {0x74, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30}\n            },\n            new PatchInfo\n            {\n                Version = \"2018.4\",\n                LightPattern = new byte[] {0x75, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30},\n                DarkPattern = new byte[] {0x74, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.2\",\n                LightPattern = new byte[] {0x75, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49},\n                DarkPattern = new byte[] {0x74, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.3\",\n                LightPattern = new byte[] {0x75, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49},\n                DarkPattern = new byte[] {0x74, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.4\",\n                LightPattern = new byte[] {0x75, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49},\n                DarkPattern = new byte[] {0x74, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49}\n            },\n            new PatchInfo\n            {\n                Version = \"2020.1\",\n                LightPattern = new byte[] {0x75, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49},\n                DarkPattern = new byte[] {0x74, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49}\n            }\n        };\n        \n        private static readonly List<PatchInfo> MacPatches = new List<PatchInfo>\n        {\n            new PatchInfo\n            {\n                Version = \"2018.4\",\n                DarkPattern = new byte[] {0x75, 0x03, 0x41, 0x8B, 0x06, 0x4C, 0x3B},\n                LightPattern = new byte[] {0x74, 0x03, 0x41, 0x8B, 0x06, 0x4C, 0x3B}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.1\",\n                DarkPattern = new byte[] {0x75, 0x03, 0x41, 0x8b, 0x06, 0x48},\n                LightPattern = new byte[] {0x74, 0x03, 0x41, 0x8b, 0x06, 0x48}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.2\",\n                DarkPattern = new byte[] {0x75, 0x04, 0x8b, 0x03, 0xeb, 0x02},\n                LightPattern = new byte[] {0x74, 0x04, 0x8b, 0x03, 0xeb, 0x02}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.3\",\n                DarkPattern = new byte[] {0x85, 0xD5, 0x00, 0x00, 0x00, 0x8B, 0x03},\n                LightPattern = new byte[] {0x84, 0xD5, 0x00, 0x00, 0x00, 0x8B, 0x03}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.4\",\n                DarkPattern = new byte[] {0x85, 0xD5, 0x00, 0x00, 0x00, 0x8B, 0x03},\n                LightPattern = new byte[] {0x84, 0xD5, 0x00, 0x00, 0x00, 0x8B, 0x03}\n            },\n            new PatchInfo\n            {\n                Version = \"2020.1\",\n                DarkPattern = new byte[] {0x75, 0x5E, 0x8B, 0x03, 0xEB},\n                LightPattern = new byte[] {0x74, 0x5E, 0x8B, 0x03, 0xEB}\n            },\n            new PatchInfo\n            {\n                Version = \"2020.2\",\n                DarkPattern = new byte[] {0x75, 0x5E, 0x8B, 0x03, 0xEB},\n                LightPattern = new byte[] {0x74, 0x5E, 0x8B, 0x03, 0xEB}\n            }\n        };\n        \n        private static readonly List<PatchInfo> LinuxPatches = new List<PatchInfo>\n        {\n            new PatchInfo\n            {\n                Version = \"2018.4\",\n                DarkPattern = new byte[] {0x75, 0x03, 0x41, 0x8b, 0x06, 0x48},\n                LightPattern = new byte[] {0x74, 0x03, 0x41, 0x8b, 0x06, 0x48}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.1\",\n                DarkPattern = new byte[] {0x75, 0x04, 0x41, 0x8b, 0x45, 0x00, 0x43, 0x83},\n                LightPattern = new byte[] {0x74, 0x04, 0x41, 0x8b, 0x45, 0x00, 0x43, 0x83}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.2\",\n                DarkPattern = new byte[] {0x75, 0x02, 0x8b, 0x03, 0x48, 0x83},\n                LightPattern = new byte[] {0x74, 0x02, 0x8b, 0x03, 0x48, 0x83}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.3\",\n                DarkPattern = new byte[] {0x75, 0x06, 0x41, 0x8b, 0x04, 0x24, 0xeb, 0x02},\n                LightPattern = new byte[] {0x74, 0x06, 0x41, 0x8b, 0x04, 0x24, 0xeb, 0x02}\n            },\n            new PatchInfo\n            {\n                Version = \"2019.4\",\n                DarkPattern = new byte[] {0x75, 0x06, 0x41, 0x8b, 0x04, 0x24, 0xeb, 0x02},\n                LightPattern = new byte[] {0x74, 0x06, 0x41, 0x8b, 0x04, 0x24, 0xeb, 0x02}\n            },\n            new PatchInfo\n            {\n                Version = \"2020.1\",\n                DarkPattern = new byte[] {0x75, 0x05, 0x41, 0x8b, 0x07, 0xeb, 0x02},\n                LightPattern = new byte[] {0x74, 0x05, 0x41, 0x8b, 0x07, 0xeb, 0x02}\n            },\n            new PatchInfo\n            {\n                Version = \"2020.2\",\n                DarkPattern = new byte[] {0x75, 0x5e, 0x8b, 0x03, 0xeb, 0x5c, 0x48},\n                LightPattern = new byte[] {0x74, 0x5e, 0x8b, 0x03, 0xeb, 0x5c, 0x48}\n            }\n        };\n\n        public static List<PatchInfo> GetPatches(OperatingSystem os)\n        {\n            var patches = os switch\n            {\n                OperatingSystem.Windows => WindowsPatches,\n                OperatingSystem.MacOS => MacPatches,\n                OperatingSystem.Linux => LinuxPatches,\n                _ => throw new ArgumentOutOfRangeException(nameof(os))\n            };\n\n            return patches.OrderByDescending(info => info.Version).ToList();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Patcher/Program.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing NDesk.Options;\nusing System.Runtime.InteropServices;\n\nnamespace Patcher\n{\n    internal static class Program\n    {\n        internal static void Main(string[] args)\n        {\n            var themeName = string.Empty;\n            var help = false;\n            var fileLocation = string.Empty;\n            var os = OperatingSystem.Unknown;\n            var version = string.Empty;\n            var force = false;\n\n            var optionSet = new OptionSet\n            {\n                {\"theme=|t=\", \"The theme to be applied to the Unity.\", v => themeName = v},\n                {\n                    \"exe=|e=\", \"The location of the Unity Editor executable.\",\n                    v => fileLocation = v\n                },\n                {\n                    \"mac\", \"Specifies if the specified binary is the MacOS version of Unity3D.\",\n                    v =>\n                    {\n                        if (v != null) os = OperatingSystem.MacOS;\n                    }\n                },\n                {\n                    \"linux\", \"Specifies if the specified binary is the Linux version of Unity3D.\",\n                    v =>\n                    {\n                        if (v != null) os = OperatingSystem.Linux;\n                    }\n                },\n                {\n                    \"windows\", \"Specifies if the specified binary is the Windows version of Unity3D.\",\n                    v =>\n                    {\n                        if (v != null) os = OperatingSystem.Windows;\n                    }\n                },\n                {\n                    \"version=|v=\", \"The version of Unity to patch.\", v => version = v\n                },\n                {\n                    \"force|f\", \"Gently applies force.\", v => force = v != null\n                },\n                {\"help|h\", v => help = v != null}\n            };\n\n            string error = null;\n            try\n            {\n                var leftover = optionSet.Parse(args);\n                if (leftover.Any())\n                    error = \"Unknown arguments: \" + string.Join(\" \", leftover);\n            }\n            catch (OptionException ex)\n            {\n                error = ex.Message;\n            }\n\n            if (error != null)\n            {\n                Console.WriteLine($\"Unity Patcher: {error}\");\n                Console.WriteLine(\"Use --help for a list of supported options.\");\n                return;\n            }\n\n            if (string.IsNullOrEmpty(themeName))\n                themeName = \"dark\";\n\n            if (!themeName.Equals(\"dark\", StringComparison.OrdinalIgnoreCase) &&\n                !themeName.Equals(\"light\", StringComparison.OrdinalIgnoreCase))\n            {\n                Console.WriteLine($\"Theme \\\"{themeName}\\\" is not supported - please specify either dark or light.\");\n                return;\n            }\n\n            if (help)\n            {\n                Console.WriteLine(\"Usage: patcher.exe\");\n                optionSet.WriteOptionDescriptions(Console.Out);\n                return;\n            }\n\n            if (os == OperatingSystem.Unknown)\n            {\n                os = DetectOperatingSystem();\n                if (os == OperatingSystem.Unknown)\n                {\n                    Console.WriteLine(\n                        \"Failed to detect OS and non was specified - please specify a valid operating system. See patcher -h for available options.\");\n                    return;\n                }\n            }\n\n            var patches = Patches.GetPatches(os);\n\n            var patch = patches.FirstOrDefault(p => p.Version.Equals(version, StringComparison.OrdinalIgnoreCase));\n\n            if (string.IsNullOrEmpty(fileLocation))\n            {\n                // https://docs.unity3d.com/Manual/GettingStartedInstallingHub.html\n                var unityInstallations = UnityInstallation.GetUnityInstallations(os);\n\n                Console.WriteLine(\"Please choose the editor which should get patched:\");\n                UnityInstallation selectedInstallation = ConsoleUtility.ShowSelectionMenu(unityInstallations.ToList(),\n                    installation =>\n                    {\n                        var supported = installation.IsSupported(patches) ? \"Supported\" : \"Unsupported\";\n                        return $\"{installation.Version}\\t({supported})\";\n                    });\n\n                fileLocation = selectedInstallation.ExecutablePath();\n                version = selectedInstallation.Version;\n\n                patch ??= selectedInstallation.GetPatch(patches);\n\n                if (patch == null)\n                {\n                    Console.WriteLine(\"Couldn't find Patch for specified Unity Installation, please choose one:\");\n                    patch = ConsoleUtility.ShowSelectionMenu(patches, patchInfo => patchInfo.Version);\n                }\n            }\n\n            if (patch == null)\n            {\n                patch = patches.First();\n                Console.WriteLine(string.IsNullOrWhiteSpace(version)\n                    ? $\"Version not explicitly specified -- defaulting to version {patch.Version} for {os}\"\n                    : $\"Could not find patch details for {os} Unity version {version} -- defaulting to version {patch.Version}.\");\n            }\n            else\n            {\n                Console.WriteLine($\"Applying Patch for {patch.Version}\");\n            }\n\n            Console.WriteLine($\"Opening Unity executable from {fileLocation}...\");\n\n            try\n            {\n                var fileInfo = new FileInfo(fileLocation);\n\n                if (!fileInfo.Exists)\n                {\n                    Console.WriteLine($\"Could not find the specified file: {fileInfo.FullName}\");\n                    return;\n                }\n\n                if (fileInfo.IsReadOnly)\n                {\n                    Console.WriteLine(\"Can not patch the specified file - it is marked as read only!\");\n                    return;\n                }\n\n                using var fs = File.Open(fileInfo.FullName, FileMode.Open, FileAccess.ReadWrite);\n                using var ms = new MemoryStream();\n\n                fs.CopyTo(ms);\n\n                CreateBackup(fileInfo, ms);\n                PatchExecutable(ms, fs, patch, themeName, force);\n            }\n            catch (UnauthorizedAccessException)\n            {\n                Console.WriteLine(\n                    \"Could not open the Unity executable - are you running the patcher as an administrator?\");\n            }\n        }\n\n        private static void CreateBackup(FileSystemInfo fileInfo, MemoryStream ms)\n        {\n            Console.WriteLine(\"Creating backup...\");\n\n            var backupFileInfo = new FileInfo(fileInfo.FullName + \".bak\");\n\n            if (backupFileInfo.Exists)\n                backupFileInfo.Delete();\n\n            using var backupWriteStream = backupFileInfo.OpenWrite();\n            \n                backupWriteStream.Write(ms.ToArray(), 0, (int)ms.Length);\n            \n\n            if (backupFileInfo.Exists)\n                Console.WriteLine($\"Backup '{backupFileInfo.Name}' created.\");\n        }\n\n        private static void PatchExecutable(MemoryStream ms, FileStream fs, PatchInfo patch, string themeName,\n            bool force)\n        {\n            Console.WriteLine(\"Searching for theme offset...\");\n\n            var buffer = ms.ToArray();\n\n            var lightOffsets = FindPattern(patch.LightPattern, buffer).ToArray();\n            var darkOffsets = FindPattern(patch.DarkPattern, buffer).ToArray();\n            var offsets = new HashSet<int>(lightOffsets);\n            offsets.UnionWith(darkOffsets);\n\n            var found = offsets.Any();\n            if (!found)\n            {\n                Console.WriteLine(\"Error: Could not find the theme offset in the specified executable!\");\n                return;\n            }\n\n            var foundMultipleOffsets = offsets.Count > 1;\n            if (foundMultipleOffsets)\n            {\n                Console.WriteLine(\n                    $\"Warning: Found more than one occurrence of the theme offset in the specified executable. There is a chance that patching it leads to undefined behaviour. It could also just work fine.{Environment.NewLine}{Environment.NewLine}\");\n                Console.WriteLine(\n                    \"Run the patcher with the --force option if you want to patch regardless of this warning.\");\n\n                if (!force)\n                    return;\n            }\n\n            var themeBytes = themeName == \"dark\" ? patch.DarkPattern : patch.LightPattern;\n\n            Console.WriteLine($\"Patching to {themeName}...\");\n\n            foreach (var offset in offsets)\n            \n                for (var i = 0; i < themeBytes.Length; i++)\n                {\n                    fs.Position = offset + i;\n                    fs.WriteByte(themeBytes[i]);\n                }\n            \n\n            Console.WriteLine(\"Unity was successfully patched. Enjoy!\");\n        }\n\n        private static IEnumerable<int> FindPattern(byte[] needle, byte[] haystack)\n        {\n            return new BinarySearcher(needle).Search(haystack).ToArray();\n        }\n\n        private static OperatingSystem DetectOperatingSystem()\n        {\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                return OperatingSystem.Windows;\n            }\n\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))\n            {\n                return OperatingSystem.MacOS;\n            }\n\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))\n            {\n                return OperatingSystem.Linux;\n            }\n\n            return OperatingSystem.Unknown;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Patcher/UnityInstallation.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text.RegularExpressions;\n\nnamespace Patcher\n{\n    /// <summary>\n    /// Represents an Unity Installation on the Disk. \n    /// </summary>\n    public class UnityInstallation\n    {\n        private readonly string _installationLocation;\n\n        private readonly OperatingSystem _operatingSystem;\n\n        /// <summary>\n        /// Creates a new Unity installation\n        /// </summary>\n        /// <param name=\"installationLocation\">The Path to this specific installation on the File System</param>\n        /// <param name=\"operatingSystem\"></param>\n        public UnityInstallation(string installationLocation, OperatingSystem operatingSystem)\n        {\n            _installationLocation = installationLocation ?? throw new ArgumentNullException(nameof(installationLocation));\n            _operatingSystem = operatingSystem;\n        }\n\n        /// <summary>\n        /// The Version of the Unity Installation\n        /// </summary>\n        public string Version => Path.GetFileName(_installationLocation);\n\n        /// <summary>\n        /// Gets the Path to the Unity executable on the disk.\n        /// </summary>\n        /// <returns></returns>\n        public string ExecutablePath() => Path.Combine(_installationLocation, _operatingSystem switch\n        {\n            OperatingSystem.Windows => @\"Editor\\Unity.exe\",\n            OperatingSystem.MacOS => \"Unity.app/Contents/MacOS/Unity\",\n            OperatingSystem.Linux => \"Unity\",\n            _ => throw new ArgumentOutOfRangeException(nameof(_operatingSystem))\n        });\n\n        public bool IsSupported(IEnumerable<PatchInfo> patches)\n        {\n            return GetPatch(patches) != null;\n        }\n\n        public PatchInfo GetPatch(IEnumerable<PatchInfo> patches)\n        {\n            foreach (PatchInfo patch in patches)\n            {\n                if (Regex.IsMatch(Version, patch.Version))\n                {\n                    return patch;\n                }\n            }\n\n            return null;\n        }\n\n        public static IEnumerable<UnityInstallation> GetUnityInstallations(OperatingSystem operatingSystem)\n        {\n            var path = operatingSystem switch\n            {\n                OperatingSystem.Windows => @\"C:\\Program Files\\Unity\\Hub\\Editor\\\",\n                OperatingSystem.MacOS => \"/Applications/Unity/Hub/Editor\",\n                OperatingSystem.Linux => \"~/Unity/Hub/Editor\",\n                _ => throw new ArgumentOutOfRangeException(nameof(operatingSystem))\n            };\n\n            if (!Directory.Exists(path)) yield break;\n            \n            var directories = Directory.GetDirectories(path);\n            Array.Sort(directories);\n            foreach (string directory in directories)\n            {\n                yield return new UnityInstallation(directory, operatingSystem);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Patcher.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio 15\nVisualStudioVersion = 15.0.28010.2041\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Patcher\", \"Patcher\\Patcher.csproj\", \"{9C373F82-C468-43A9-BCE7-2ADFF32D195C}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{9C373F82-C468-43A9-BCE7-2ADFF32D195C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{9C373F82-C468-43A9-BCE7-2ADFF32D195C}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{9C373F82-C468-43A9-BCE7-2ADFF32D195C}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{9C373F82-C468-43A9-BCE7-2ADFF32D195C}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {30DFC04F-53D2-4ACB-B3B1-EFB8FF0E2B3D}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "tools/global.json",
    "content": "{\n  \"sdk\": {\n    \"version\": \"3.1.201\"\n  }\n}"
  },
  {
    "path": "tools/publish.ps1",
    "content": "dotnet publish ../src/Patcher.sln -o=\"./publish/win-x64\" -f=\"netcoreapp3.1\" -p:PublishSingleFile=true  -c=Release --runtime win-x64\ndotnet publish ../src/Patcher.sln -o=\"./publish/osx-64\" -f=\"netcoreapp3.1\" -p:PublishSingleFile=true -c=Release --runtime osx-x64\ndotnet publish ../src/Patcher.sln -o=\"./publish/linux-x64\" -f=\"netcoreapp3.1\" -p:PublishSingleFile=true -c=Release --runtime linux-x64"
  }
]