Repository: aevitas/unity-patch
Branch: master
Commit: cb3f95ca2602
Files: 18
Total size: 77.6 KB
Directory structure:
gitextract_jzgj9f8a/
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── incompatible-version.md
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── README.md
├── src/
│ ├── Patcher/
│ │ ├── BinarySearcher.cs
│ │ ├── ConsoleUtility.cs
│ │ ├── OperatingSystem.cs
│ │ ├── Options.cs
│ │ ├── PatchInfo.cs
│ │ ├── Patcher.csproj
│ │ ├── Patches.cs
│ │ ├── Program.cs
│ │ └── UnityInstallation.cs
│ └── Patcher.sln
└── tools/
├── global.json
└── publish.ps1
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Report an issue with the patcher
title: ''
labels: ''
assignees: ''
---
**Description**
A clear and concise description of what the bug is.
**Version Information**
* What Operating System are you running the patcher on?
* What Unity Version are you using?
**Diagnostics**
What does the patcher output instead of the expected behaviour?
**Arguments**
Please provide the exact command line arguments you are using the patcher with, e.g.:
`sudo ./linux-x64/Patcher -e=/path/to/Unity -t=dark -linux`
================================================
FILE: .github/ISSUE_TEMPLATE/incompatible-version.md
================================================
---
name: Incompatible Version
about: Report an issue related to patcher compatibility
title: ''
labels: ''
assignees: ''
---
**Description**
A clear and concise description of what the bug is.
**Version Information**
* What Operating System are you running the patcher on?
* What Unity Version are you using?
**Diagnostics**
What does the patcher output instead of the expected behaviour?
**Arguments**
Please provide the exact command line arguments you are using the patcher with, e.g.:
`sudo ./linux-x64/Patcher -e=/path/to/Unity -t=dark -linux`
**Binaries**
If possible, provide a link to the Unity binary that corresponds with the version and OS you're using.
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Setup
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.201
- name: Build
run: dotnet build ./src/Patcher.sln --configuration Release
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
/Patcher/Properties/launchSettings.json
# macOS
.DS_Store
================================================
FILE: README.md
================================================

Unity Patch
===========
This repository contains a patch for Unity that allows you to set options inaccessible from the application's menus.
Currently, the only supported option for the patch is switching between the dark and light themes in Unity.
Usage
=====
We provide binaries for Windows 10, Linux, and macOS. All compiled binaries are x64.
See the [release section](https://github.com/aevitas/unity-patch/releases).
Alternatively, you can build the patch from source.
Run `patcher.exe` on windows, or alternatively `Patcher` on Linux or MacOS. By default, it will locate your Unity install
at `C:\Program Files\Unity\Editor\Unity.exe`, which is obviously wrong for both Linux and macOS, and it will set your theme to dark.
You can pass various arguments to the patcher:
* `exe=` or `e=` to specify the location of the Unity executable
* `theme=` or `t=` to set the theme, currently only `light` or `dark` are valid
* `help` or `h` to display the options the patcher supports
* `--windows` for Windows builds of Unity
* `--linux` for Linux builds of Unity
* `--mac` for MacOS builds of Unity
* `--force` or `--f` to gently apply force
Depending on your system, looking up the offsets to patch can take a couple moments.
Unity Versions
--------------
The patcher supports multiple versions of Unity. Versions can be specified by passing the `-v=` or `--version=` command line argument.
For instance, if you want to patch Unity version 2020.1 on Windows, you'd run:
```
patcher.exe --windows --version=2020.1 --t=dark
```
Currently, the following OS and Unity version combinations are supported:
| | Windows | MacOS | Linux |
|---------|:------------------:|:------------------:|:------------------:|
| 2020.2a | :x: | :white_check_mark: | :white_check_mark: |
| 2020.1b | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| 2019.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| 2019.3 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| 2019.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| 2019.1 | :x: | :white_check_mark: | :white_check_mark: |
| 2018.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| 2018.3 | :white_check_mark: | :x: | :x: |
| 2018.2 | :white_check_mark: | :x: | :x: |
If you don't specify a version, the patcher will select the most recent version for your operating system.
Troubleshooting
===============
To 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:
* Resetting your user preferences either manually or by calling [`EditorPrefs.DeleteAll()`](https://github.com/aevitas/unity-patch/issues/17#issuecomment-592070343)
* 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.
* 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.
Issues
======
If 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.
Linux and MacOS
===============
When 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.
* Mac users should run the patcher with the `--mac` command line option
* Linux users should run the patcher with the `--linux` command line option
For example, on Linux you would run:
`sudo ./linux-x64/Patcher -e=/path/to/Unity --t=dark --linux`
or on Mac:
`sudo ./osx-64/Patcher -e=/Applications/Unity/Hub/Editor/<VERSION>/Unity.app/Contents/MacOS/Unity --mac --t=dark`
================================================
FILE: src/Patcher/BinarySearcher.cs
================================================
using System;
using System.Collections.Generic;
namespace Patcher
{
/// <summary>
/// Original by Matthew Watson from https://stackoverflow.com/questions/37500629/find-byte-sequence-within-a-byte-array
/// </summary>
public sealed class BinarySearcher
{
readonly byte[] needle;
readonly int[] charTable;
readonly int[] offsetTable;
public BinarySearcher(byte[] needle)
{
this.needle = needle;
this.charTable = makeByteTable(needle);
this.offsetTable = makeOffsetTable(needle);
}
public IEnumerable<int> Search(byte[] haystack)
{
if (needle.Length == 0)
yield break;
for (int i = needle.Length - 1; i < haystack.Length;)
{
int j;
for (j = needle.Length - 1; needle[j] == haystack[i]; --i, --j)
{
if (j != 0)
continue;
yield return i;
i += needle.Length - 1;
break;
}
i += Math.Max(offsetTable[needle.Length - 1 - j], charTable[haystack[i]]);
}
}
static int[] makeByteTable(byte[] needle)
{
int[] table = new int[256];
for (int i = 0; i < table.Length; ++i)
table[i] = needle.Length;
for (int i = 0; i < needle.Length - 1; ++i)
table[needle[i]] = needle.Length - 1 - i;
return table;
}
static int[] makeOffsetTable(byte[] needle)
{
int[] table = new int[needle.Length];
int lastPrefixPosition = needle.Length;
for (int i = needle.Length - 1; i >= 0; --i)
{
if (isPrefix(needle, i + 1))
lastPrefixPosition = i + 1;
table[needle.Length - 1 - i] = lastPrefixPosition - i + needle.Length - 1;
}
for (int i = 0; i < needle.Length - 1; ++i)
{
int slen = suffixLength(needle, i);
table[slen] = needle.Length - 1 - i + slen;
}
return table;
}
static bool isPrefix(byte[] needle, int p)
{
for (int i = p, j = 0; i < needle.Length; ++i, ++j)
if (needle[i] != needle[j])
return false;
return true;
}
static int suffixLength(byte[] needle, int p)
{
int len = 0;
for (int i = p, j = needle.Length - 1; i >= 0 && needle[i] == needle[j]; --i, --j)
++len;
return len;
}
}
}
================================================
FILE: src/Patcher/ConsoleUtility.cs
================================================
using System;
using System.Collections.Generic;
namespace Patcher
{
/// <summary>
/// Utility methods for interacting with the console.
/// </summary>
public static class ConsoleUtility
{
/// <summary>
/// Displays a selection menu in the console to the user which can be navigated using the arrow keys and enter
/// to select the desired option from the list.
/// </summary>
/// <param name="options">The list of options from which the user can choose</param>
/// <param name="optionLine">
/// A function taking one of the options an returning a string representation of how it should be displayed to the user
/// </param>
/// <typeparam name="T">The type of a single option</typeparam>
/// <returns>The selected option from the list</returns>
public static T ShowSelectionMenu<T>(List<T> options, Func<T, string> optionLine)
{
int selected = 0;
bool done = false;
while (!done)
{
for (int i = 0; i < options.Count; i++)
{
if (i == selected)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("> ");
}
else
{
Console.Write(" ");
}
Console.WriteLine(optionLine(options[i]));
Console.ResetColor();
}
switch (Console.ReadKey(true).Key)
{
case ConsoleKey.UpArrow:
selected = Math.Max(0, selected - 1);
break;
case ConsoleKey.DownArrow:
selected = Math.Min(options.Count - 1, selected + 1);
break;
case ConsoleKey.Spacebar:
case ConsoleKey.Enter:
done = true;
break;
}
if (!done)
Console.CursorTop -= options.Count;
}
return options[selected];
}
}
}
================================================
FILE: src/Patcher/OperatingSystem.cs
================================================
namespace Patcher
{
public enum OperatingSystem
{
Unknown,
Windows,
MacOS,
Linux
}
}
================================================
FILE: src/Patcher/Options.cs
================================================
//
// Options.cs
//
// Authors:
// Jonathan Pryor <jpryor@novell.com>
//
// Copyright (C) 2008 Novell (http://www.novell.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// Compile With:
// gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll
// gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll
//
// The LINQ version just changes the implementation of
// OptionSet.Parse(IEnumerable<string>), and confers no semantic changes.
//
// A Getopt::Long-inspired option parsing library for C#.
//
// NDesk.Options.OptionSet is built upon a key/value table, where the
// key is a option format string and the value is a delegate that is
// invoked when the format string is matched.
//
// Option format strings:
// Regex-like BNF Grammar:
// name: .+
// type: [=:]
// sep: ( [^{}]+ | '{' .+ '}' )?
// aliases: ( name type sep ) ( '|' name type sep )*
//
// Each '|'-delimited name is an alias for the associated action. If the
// format string ends in a '=', it has a required value. If the format
// string ends in a ':', it has an optional value. If neither '=' or ':'
// is present, no value is supported. `=' or `:' need only be defined on one
// alias, but if they are provided on more than one they must be consistent.
//
// Each alias portion may also end with a "key/value separator", which is used
// to split option values if the option accepts > 1 value. If not specified,
// it defaults to '=' and ':'. If specified, it can be any character except
// '{' and '}' OR the *string* between '{' and '}'. If no separator should be
// used (i.e. the separate values should be distinct arguments), then "{}"
// should be used as the separator.
//
// Options are extracted either from the current option by looking for
// the option name followed by an '=' or ':', or is taken from the
// following option IFF:
// - The current option does not contain a '=' or a ':'
// - The current option requires a value (i.e. not a Option type of ':')
//
// The `name' used in the option format string does NOT include any leading
// option indicator, such as '-', '--', or '/'. All three of these are
// permitted/required on any named option.
//
// Option bundling is permitted so long as:
// - '-' is used to start the option group
// - all of the bundled options are a single character
// - at most one of the bundled options accepts a value, and the value
// provided starts from the next character to the end of the string.
//
// This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value'
// as '-Dname=value'.
//
// Option processing is disabled by specifying "--". All options after "--"
// are returned by OptionSet.Parse() unchanged and unprocessed.
//
// Unprocessed options are returned from OptionSet.Parse().
//
// Examples:
// int verbose = 0;
// OptionSet p = new OptionSet ()
// .Add ("v", v => ++verbose)
// .Add ("name=|value=", v => Console.WriteLine (v));
// p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"});
//
// The above would parse the argument string array, and would invoke the
// lambda expression three times, setting `verbose' to 3 when complete.
// It would also print out "A" and "B" to standard output.
// The returned array would contain the string "extra".
//
// C# 3.0 collection initializers are supported and encouraged:
// var p = new OptionSet () {
// { "h|?|help", v => ShowHelp () },
// };
//
// System.ComponentModel.TypeConverter is also supported, allowing the use of
// custom data types in the callback type; TypeConverter.ConvertFromString()
// is used to convert the value option to an instance of the specified
// type:
//
// var p = new OptionSet () {
// { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) },
// };
//
// Random other tidbits:
// - Boolean options (those w/o '=' or ':' in the option format string)
// are explicitly enabled if they are followed with '+', and explicitly
// disabled if they are followed with '-':
// string a = null;
// var p = new OptionSet () {
// { "a", s => a = s },
// };
// p.Parse (new string[]{"-a"}); // sets v != null
// p.Parse (new string[]{"-a+"}); // sets v != null
// p.Parse (new string[]{"-a-"}); // sets v == null
//
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
#if LINQ
using System.Linq;
#endif
#if TEST
using NDesk.Options;
#endif
namespace NDesk.Options
{
public class OptionValueCollection : IList, IList<string>
{
List<string> values = new List<string>();
OptionContext c;
internal OptionValueCollection(OptionContext c)
{
this.c = c;
}
#region ICollection
void ICollection.CopyTo(Array array, int index) { (values as ICollection).CopyTo(array, index); }
bool ICollection.IsSynchronized { get { return (values as ICollection).IsSynchronized; } }
object ICollection.SyncRoot { get { return (values as ICollection).SyncRoot; } }
#endregion
#region ICollection<T>
public void Add(string item) { values.Add(item); }
public void Clear() { values.Clear(); }
public bool Contains(string item) { return values.Contains(item); }
public void CopyTo(string[] array, int arrayIndex) { values.CopyTo(array, arrayIndex); }
public bool Remove(string item) { return values.Remove(item); }
public int Count { get { return values.Count; } }
public bool IsReadOnly { get { return false; } }
#endregion
#region IEnumerable
IEnumerator IEnumerable.GetEnumerator() { return values.GetEnumerator(); }
#endregion
#region IEnumerable<T>
public IEnumerator<string> GetEnumerator() { return values.GetEnumerator(); }
#endregion
#region IList
int IList.Add(object value) { return (values as IList).Add(value); }
bool IList.Contains(object value) { return (values as IList).Contains(value); }
int IList.IndexOf(object value) { return (values as IList).IndexOf(value); }
void IList.Insert(int index, object value) { (values as IList).Insert(index, value); }
void IList.Remove(object value) { (values as IList).Remove(value); }
void IList.RemoveAt(int index) { (values as IList).RemoveAt(index); }
bool IList.IsFixedSize { get { return false; } }
object IList.this[int index] { get { return this[index]; } set { (values as IList)[index] = value; } }
#endregion
#region IList<T>
public int IndexOf(string item) { return values.IndexOf(item); }
public void Insert(int index, string item) { values.Insert(index, item); }
public void RemoveAt(int index) { values.RemoveAt(index); }
private void AssertValid(int index)
{
if (c.Option == null)
throw new InvalidOperationException("OptionContext.Option is null.");
if (index >= c.Option.MaxValueCount)
throw new ArgumentOutOfRangeException("index");
if (c.Option.OptionValueType == OptionValueType.Required &&
index >= values.Count)
throw new OptionException(string.Format(
c.OptionSet.MessageLocalizer("Missing required value for option '{0}'."), c.OptionName),
c.OptionName);
}
public string this[int index]
{
get
{
AssertValid(index);
return index >= values.Count ? null : values[index];
}
set
{
values[index] = value;
}
}
#endregion
public List<string> ToList()
{
return new List<string>(values);
}
public string[] ToArray()
{
return values.ToArray();
}
public override string ToString()
{
return string.Join(", ", values.ToArray());
}
}
public class OptionContext
{
private Option option;
private string name;
private int index;
private readonly OptionSet set;
private readonly OptionValueCollection c;
public OptionContext(OptionSet set)
{
this.set = set;
this.c = new OptionValueCollection(this);
}
public Option Option
{
get { return option; }
set { option = value; }
}
public string OptionName
{
get { return name; }
set { name = value; }
}
public int OptionIndex
{
get { return index; }
set { index = value; }
}
public OptionSet OptionSet
{
get { return set; }
}
public OptionValueCollection OptionValues
{
get { return c; }
}
}
public enum OptionValueType
{
None,
Optional,
Required,
}
public abstract class Option
{
readonly string prototype, description;
string[] names;
readonly OptionValueType type;
readonly int count;
string[] separators;
protected Option(string prototype, string description)
: this(prototype, description, 1)
{
}
protected Option(string prototype, string description, int maxValueCount)
{
if (prototype == null)
throw new ArgumentNullException("prototype");
if (prototype.Length == 0)
throw new ArgumentException("Cannot be the empty string.", "prototype");
if (maxValueCount < 0)
throw new ArgumentOutOfRangeException("maxValueCount");
this.prototype = prototype;
this.names = prototype.Split('|');
this.description = description;
this.count = maxValueCount;
this.type = ParsePrototype();
if (this.count == 0 && type != OptionValueType.None)
throw new ArgumentException(
"Cannot provide maxValueCount of 0 for OptionValueType.Required or " +
"OptionValueType.Optional.",
"maxValueCount");
if (this.type == OptionValueType.None && maxValueCount > 1)
throw new ArgumentException(
string.Format("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount),
"maxValueCount");
if (Array.IndexOf(names, "<>") >= 0 &&
((names.Length == 1 && this.type != OptionValueType.None) ||
(names.Length > 1 && this.MaxValueCount > 1)))
throw new ArgumentException(
"The default option handler '<>' cannot require values.",
"prototype");
}
public string Prototype { get { return prototype; } }
public string Description { get { return description; } }
public OptionValueType OptionValueType { get { return type; } }
public int MaxValueCount { get { return count; } }
public string[] GetNames()
{
return (string[])names.Clone();
}
public string[] GetValueSeparators()
{
if (separators == null)
return new string[0];
return (string[])separators.Clone();
}
protected static T Parse<T>(string value, OptionContext c)
{
TypeConverter conv = TypeDescriptor.GetConverter(typeof(T));
T t = default;
try
{
if (value != null)
t = (T)conv.ConvertFromString(value);
}
catch (Exception e)
{
throw new OptionException(
string.Format(
c.OptionSet.MessageLocalizer("Could not convert string `{0}' to type {1} for option `{2}'."),
value, typeof(T).Name, c.OptionName),
c.OptionName, e);
}
return t;
}
internal string[] Names { get { return names; } }
internal string[] ValueSeparators { get { return separators; } }
static readonly char[] NameTerminator = new char[] { '=', ':' };
private OptionValueType ParsePrototype()
{
char type = '\0';
List<string> seps = new List<string>();
for (int i = 0; i < names.Length; ++i)
{
string name = names[i];
if (name.Length == 0)
throw new ArgumentException("Empty option names are not supported.", "prototype");
int end = name.IndexOfAny(NameTerminator);
if (end == -1)
continue;
names[i] = name.Substring(0, end);
if (type == '\0' || type == name[end])
type = name[end];
else
throw new ArgumentException(
string.Format("Conflicting option types: '{0}' vs. '{1}'.", type, name[end]),
"prototype");
AddSeparators(name, end, seps);
}
if (type == '\0')
return OptionValueType.None;
if (count <= 1 && seps.Count != 0)
throw new ArgumentException(
string.Format("Cannot provide key/value separators for Options taking {0} value(s).", count),
"prototype");
if (count > 1)
{
if (seps.Count == 0)
this.separators = new string[] { ":", "=" };
else if (seps.Count == 1 && seps[0].Length == 0)
this.separators = null;
else
this.separators = seps.ToArray();
}
return type == '=' ? OptionValueType.Required : OptionValueType.Optional;
}
private static void AddSeparators(string name, int end, ICollection<string> seps)
{
int start = -1;
for (int i = end + 1; i < name.Length; ++i)
{
switch (name[i])
{
case '{':
if (start != -1)
throw new ArgumentException(
string.Format("Ill-formed name/value separator found in \"{0}\".", name),
"prototype");
start = i + 1;
break;
case '}':
if (start == -1)
throw new ArgumentException(
string.Format("Ill-formed name/value separator found in \"{0}\".", name),
"prototype");
seps.Add(name.Substring(start, i - start));
start = -1;
break;
default:
if (start == -1)
seps.Add(name[i].ToString());
break;
}
}
if (start != -1)
throw new ArgumentException(
string.Format("Ill-formed name/value separator found in \"{0}\".", name),
"prototype");
}
public void Invoke(OptionContext c)
{
OnParseComplete(c);
c.OptionName = null;
c.Option = null;
c.OptionValues.Clear();
}
protected abstract void OnParseComplete(OptionContext c);
public override string ToString()
{
return Prototype;
}
}
[Serializable]
public class OptionException : Exception
{
private readonly string option;
public OptionException()
{
}
public OptionException(string message, string optionName)
: base(message)
{
this.option = optionName;
}
public OptionException(string message, string optionName, Exception innerException)
: base(message, innerException)
{
this.option = optionName;
}
protected OptionException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
this.option = info.GetString("OptionName");
}
public string OptionName
{
get { return this.option; }
}
[SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("OptionName", option);
}
}
public delegate void OptionAction<TKey, TValue>(TKey key, TValue value);
public class OptionSet : KeyedCollection<string, Option>
{
public OptionSet()
: this(delegate (string f) { return f; })
{
}
public OptionSet(Converter<string, string> localizer)
{
this.localizer = localizer;
}
readonly Converter<string, string> localizer;
public Converter<string, string> MessageLocalizer
{
get { return localizer; }
}
protected override string GetKeyForItem(Option item)
{
if (item == null)
throw new ArgumentNullException("option");
if (item.Names != null && item.Names.Length > 0)
return item.Names[0];
// This should never happen, as it's invalid for Option to be
// constructed w/o any names.
throw new InvalidOperationException("Option has no names!");
}
[Obsolete("Use KeyedCollection.this[string]")]
protected Option GetOptionForName(string option)
{
if (option == null)
throw new ArgumentNullException("option");
try
{
return base[option];
}
catch (KeyNotFoundException)
{
return null;
}
}
protected override void InsertItem(int index, Option item)
{
base.InsertItem(index, item);
AddImpl(item);
}
protected override void RemoveItem(int index)
{
base.RemoveItem(index);
Option p = Items[index];
// KeyedCollection.RemoveItem() handles the 0th item
for (int i = 1; i < p.Names.Length; ++i)
{
Dictionary.Remove(p.Names[i]);
}
}
protected override void SetItem(int index, Option item)
{
base.SetItem(index, item);
RemoveItem(index);
AddImpl(item);
}
private void AddImpl(Option option)
{
if (option == null)
throw new ArgumentNullException("option");
List<string> added = new List<string>(option.Names.Length);
try
{
// KeyedCollection.InsertItem/SetItem handle the 0th name.
for (int i = 1; i < option.Names.Length; ++i)
{
Dictionary.Add(option.Names[i], option);
added.Add(option.Names[i]);
}
}
catch (Exception)
{
foreach (string name in added)
Dictionary.Remove(name);
throw;
}
}
public new OptionSet Add(Option option)
{
base.Add(option);
return this;
}
sealed class ActionOption : Option
{
readonly Action<OptionValueCollection> action;
public ActionOption(string prototype, string description, int count, Action<OptionValueCollection> action)
: base(prototype, description, count)
{
this.action = action ?? throw new ArgumentNullException("action");
}
protected override void OnParseComplete(OptionContext c)
{
action(c.OptionValues);
}
}
public OptionSet Add(string prototype, Action<string> action)
{
return Add(prototype, null, action);
}
public OptionSet Add(string prototype, string description, Action<string> action)
{
if (action == null)
throw new ArgumentNullException("action");
Option p = new ActionOption(prototype, description, 1,
delegate (OptionValueCollection v) { action(v[0]); });
base.Add(p);
return this;
}
public OptionSet Add(string prototype, OptionAction<string, string> action)
{
return Add(prototype, null, action);
}
public OptionSet Add(string prototype, string description, OptionAction<string, string> action)
{
if (action == null)
throw new ArgumentNullException("action");
Option p = new ActionOption(prototype, description, 2,
delegate (OptionValueCollection v) { action(v[0], v[1]); });
base.Add(p);
return this;
}
sealed class ActionOption<T> : Option
{
readonly Action<T> action;
public ActionOption(string prototype, string description, Action<T> action)
: base(prototype, description, 1)
{
this.action = action ?? throw new ArgumentNullException("action");
}
protected override void OnParseComplete(OptionContext c)
{
action(Parse<T>(c.OptionValues[0], c));
}
}
sealed class ActionOption<TKey, TValue> : Option
{
readonly OptionAction<TKey, TValue> action;
public ActionOption(string prototype, string description, OptionAction<TKey, TValue> action)
: base(prototype, description, 2)
{
this.action = action ?? throw new ArgumentNullException("action");
}
protected override void OnParseComplete(OptionContext c)
{
action(
Parse<TKey>(c.OptionValues[0], c),
Parse<TValue>(c.OptionValues[1], c));
}
}
public OptionSet Add<T>(string prototype, Action<T> action)
{
return Add(prototype, null, action);
}
public OptionSet Add<T>(string prototype, string description, Action<T> action)
{
return Add(new ActionOption<T>(prototype, description, action));
}
public OptionSet Add<TKey, TValue>(string prototype, OptionAction<TKey, TValue> action)
{
return Add(prototype, null, action);
}
public OptionSet Add<TKey, TValue>(string prototype, string description, OptionAction<TKey, TValue> action)
{
return Add(new ActionOption<TKey, TValue>(prototype, description, action));
}
protected virtual OptionContext CreateOptionContext()
{
return new OptionContext(this);
}
#if LINQ
public List<string> Parse (IEnumerable<string> arguments)
{
bool process = true;
OptionContext c = CreateOptionContext ();
c.OptionIndex = -1;
var def = GetOptionForName ("<>");
var unprocessed =
from argument in arguments
where ++c.OptionIndex >= 0 && (process || def != null)
? process
? argument == "--"
? (process = false)
: !Parse (argument, c)
? def != null
? Unprocessed (null, def, c, argument)
: true
: false
: def != null
? Unprocessed (null, def, c, argument)
: true
: true
select argument;
List<string> r = unprocessed.ToList ();
if (c.Option != null)
c.Option.Invoke (c);
return r;
}
#else
public List<string> Parse(IEnumerable<string> arguments)
{
OptionContext c = CreateOptionContext();
c.OptionIndex = -1;
bool process = true;
List<string> unprocessed = new List<string>();
Option def = Contains("<>") ? this["<>"] : null;
foreach (string argument in arguments)
{
++c.OptionIndex;
if (argument == "--")
{
process = false;
continue;
}
if (!process)
{
Unprocessed(unprocessed, def, c, argument);
continue;
}
if (!Parse(argument, c))
Unprocessed(unprocessed, def, c, argument);
}
if (c.Option != null)
c.Option.Invoke(c);
return unprocessed;
}
#endif
private static bool Unprocessed(ICollection<string> extra, Option def, OptionContext c, string argument)
{
if (def == null)
{
extra.Add(argument);
return false;
}
c.OptionValues.Add(argument);
c.Option = def;
c.Option.Invoke(c);
return false;
}
private readonly Regex ValueOption = new Regex(
@"^(?<flag>--|-|/)(?<name>[^:=]+)((?<sep>[:=])(?<value>.*))?$");
protected bool GetOptionParts(string argument, out string flag, out string name, out string sep, out string value)
{
if (argument == null)
throw new ArgumentNullException("argument");
flag = name = sep = value = null;
Match m = ValueOption.Match(argument);
if (!m.Success)
{
return false;
}
flag = m.Groups["flag"].Value;
name = m.Groups["name"].Value;
if (m.Groups["sep"].Success && m.Groups["value"].Success)
{
sep = m.Groups["sep"].Value;
value = m.Groups["value"].Value;
}
return true;
}
protected virtual bool Parse(string argument, OptionContext c)
{
if (c.Option != null)
{
ParseValue(argument, c);
return true;
}
if (!GetOptionParts(argument, out string f, out string n, out string s, out string v))
return false;
Option p;
if (Contains(n))
{
p = this[n];
c.OptionName = f + n;
c.Option = p;
switch (p.OptionValueType)
{
case OptionValueType.None:
c.OptionValues.Add(n);
c.Option.Invoke(c);
break;
case OptionValueType.Optional:
case OptionValueType.Required:
ParseValue(v, c);
break;
}
return true;
}
// no match; is it a bool option?
if (ParseBool(argument, n, c))
return true;
// is it a bundled option?
if (ParseBundledValue(f, string.Concat(n + s + v), c))
return true;
return false;
}
private void ParseValue(string option, OptionContext c)
{
if (option != null)
foreach (string o in c.Option.ValueSeparators != null
? option.Split(c.Option.ValueSeparators, StringSplitOptions.None)
: new string[] { option })
{
c.OptionValues.Add(o);
}
if (c.OptionValues.Count == c.Option.MaxValueCount ||
c.Option.OptionValueType == OptionValueType.Optional)
c.Option.Invoke(c);
else if (c.OptionValues.Count > c.Option.MaxValueCount)
{
throw new OptionException(localizer(string.Format(
"Error: Found {0} option values when expecting {1}.",
c.OptionValues.Count, c.Option.MaxValueCount)),
c.OptionName);
}
}
private bool ParseBool(string option, string n, OptionContext c)
{
Option p;
string rn;
if (n.Length >= 1 && (n[n.Length - 1] == '+' || n[n.Length - 1] == '-') &&
Contains((rn = n.Substring(0, n.Length - 1))))
{
p = this[rn];
string v = n[n.Length - 1] == '+' ? option : null;
c.OptionName = option;
c.Option = p;
c.OptionValues.Add(v);
p.Invoke(c);
return true;
}
return false;
}
private bool ParseBundledValue(string f, string n, OptionContext c)
{
if (f != "-")
return false;
for (int i = 0; i < n.Length; ++i)
{
Option p;
string opt = f + n[i].ToString();
string rn = n[i].ToString();
if (!Contains(rn))
{
if (i == 0)
return false;
throw new OptionException(string.Format(localizer(
"Cannot bundle unregistered option '{0}'."), opt), opt);
}
p = this[rn];
switch (p.OptionValueType)
{
case OptionValueType.None:
Invoke(c, opt, n, p);
break;
case OptionValueType.Optional:
case OptionValueType.Required:
{
string v = n.Substring(i + 1);
c.Option = p;
c.OptionName = opt;
ParseValue(v.Length != 0 ? v : null, c);
return true;
}
default:
throw new InvalidOperationException("Unknown OptionValueType: " + p.OptionValueType);
}
}
return true;
}
private static void Invoke(OptionContext c, string name, string value, Option option)
{
c.OptionName = name;
c.Option = option;
c.OptionValues.Add(value);
option.Invoke(c);
}
private const int OptionWidth = 29;
public void WriteOptionDescriptions(TextWriter o)
{
foreach (Option p in this)
{
int written = 0;
if (!WriteOptionPrototype(o, p, ref written))
continue;
if (written < OptionWidth)
o.Write(new string(' ', OptionWidth - written));
else
{
o.WriteLine();
o.Write(new string(' ', OptionWidth));
}
List<string> lines = GetLines(localizer(GetDescription(p.Description)));
o.WriteLine(lines[0]);
string prefix = new string(' ', OptionWidth + 2);
for (int i = 1; i < lines.Count; ++i)
{
o.Write(prefix);
o.WriteLine(lines[i]);
}
}
}
bool WriteOptionPrototype(TextWriter o, Option p, ref int written)
{
string[] names = p.Names;
int i = GetNextOptionIndex(names, 0);
if (i == names.Length)
return false;
if (names[i].Length == 1)
{
Write(o, ref written, " -");
Write(o, ref written, names[0]);
}
else
{
Write(o, ref written, " --");
Write(o, ref written, names[0]);
}
for (i = GetNextOptionIndex(names, i + 1);
i < names.Length; i = GetNextOptionIndex(names, i + 1))
{
Write(o, ref written, ", ");
Write(o, ref written, names[i].Length == 1 ? "-" : "--");
Write(o, ref written, names[i]);
}
if (p.OptionValueType == OptionValueType.Optional ||
p.OptionValueType == OptionValueType.Required)
{
if (p.OptionValueType == OptionValueType.Optional)
{
Write(o, ref written, localizer("["));
}
Write(o, ref written, localizer("=" + GetArgumentName(0, p.MaxValueCount, p.Description)));
string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0
? p.ValueSeparators[0]
: " ";
for (int c = 1; c < p.MaxValueCount; ++c)
{
Write(o, ref written, localizer(sep + GetArgumentName(c, p.MaxValueCount, p.Description)));
}
if (p.OptionValueType == OptionValueType.Optional)
{
Write(o, ref written, localizer("]"));
}
}
return true;
}
static int GetNextOptionIndex(string[] names, int i)
{
while (i < names.Length && names[i] == "<>")
{
++i;
}
return i;
}
static void Write(TextWriter o, ref int n, string s)
{
n += s.Length;
o.Write(s);
}
private static string GetArgumentName(int index, int maxIndex, string description)
{
if (description == null)
return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
string[] nameStart;
if (maxIndex == 1)
nameStart = new string[] { "{0:", "{" };
else
nameStart = new string[] { "{" + index + ":" };
for (int i = 0; i < nameStart.Length; ++i)
{
int start, j = 0;
do
{
start = description.IndexOf(nameStart[i], j);
} while (start >= 0 && j != 0 ? description[j++ - 1] == '{' : false);
if (start == -1)
continue;
int end = description.IndexOf("}", start);
if (end == -1)
continue;
return description.Substring(start + nameStart[i].Length, end - start - nameStart[i].Length);
}
return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
}
private static string GetDescription(string description)
{
if (description == null)
return string.Empty;
StringBuilder sb = new StringBuilder(description.Length);
int start = -1;
for (int i = 0; i < description.Length; ++i)
{
switch (description[i])
{
case '{':
if (i == start)
{
sb.Append('{');
start = -1;
}
else if (start < 0)
start = i + 1;
break;
case '}':
if (start < 0)
{
if ((i + 1) == description.Length || description[i + 1] != '}')
throw new InvalidOperationException("Invalid option description: " + description);
++i;
sb.Append("}");
}
else
{
sb.Append(description.Substring(start, i - start));
start = -1;
}
break;
case ':':
if (start < 0)
goto default;
start = i + 1;
break;
default:
if (start < 0)
sb.Append(description[i]);
break;
}
}
return sb.ToString();
}
private static List<string> GetLines(string description)
{
List<string> lines = new List<string>();
if (string.IsNullOrEmpty(description))
{
lines.Add(string.Empty);
return lines;
}
int length = 80 - OptionWidth - 2;
int start = 0, end;
do
{
end = GetLineEnd(start, length, description);
bool cont = false;
if (end < description.Length)
{
char c = description[end];
if (c == '-' || (char.IsWhiteSpace(c) && c != '\n'))
++end;
else if (c != '\n')
{
cont = true;
--end;
}
}
lines.Add(description.Substring(start, end - start));
if (cont)
{
lines[lines.Count - 1] += "-";
}
start = end;
if (start < description.Length && description[start] == '\n')
++start;
} while (end < description.Length);
return lines;
}
private static int GetLineEnd(int start, int length, string description)
{
int end = Math.Min(start + length, description.Length);
int sep = -1;
for (int i = start; i < end; ++i)
{
switch (description[i])
{
case ' ':
case '\t':
case '\v':
case '-':
case ',':
case '.':
case ';':
sep = i;
break;
case '\n':
return i;
}
}
if (sep == -1 || end == description.Length)
return end;
return sep;
}
}
}
================================================
FILE: src/Patcher/PatchInfo.cs
================================================
namespace Patcher
{
public class PatchInfo
{
public string Version { get; set; }
public byte[] DarkPattern { get; set; }
public byte[] LightPattern { get; set; }
}
}
================================================
FILE: src/Patcher/Patcher.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<Folder Include="Properties\PublishProfiles\" />
</ItemGroup>
</Project>
================================================
FILE: src/Patcher/Patches.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
namespace Patcher
{
public static class Patches
{
private static readonly List<PatchInfo> WindowsPatches = new List<PatchInfo>
{
new PatchInfo
{
Version = "2018.2",
LightPattern = new byte[] {0x75, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30},
DarkPattern = new byte[] {0x74, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30}
},
new PatchInfo
{
Version = "2018.3",
LightPattern = new byte[] {0x75, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30},
DarkPattern = new byte[] {0x74, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30}
},
new PatchInfo
{
Version = "2018.4",
LightPattern = new byte[] {0x75, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30},
DarkPattern = new byte[] {0x74, 0x08, 0x33, 0xC0, 0x48, 0x83, 0xC4, 0x30}
},
new PatchInfo
{
Version = "2019.2",
LightPattern = new byte[] {0x75, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49},
DarkPattern = new byte[] {0x74, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49}
},
new PatchInfo
{
Version = "2019.3",
LightPattern = new byte[] {0x75, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49},
DarkPattern = new byte[] {0x74, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49}
},
new PatchInfo
{
Version = "2019.4",
LightPattern = new byte[] {0x75, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49},
DarkPattern = new byte[] {0x74, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49}
},
new PatchInfo
{
Version = "2020.1",
LightPattern = new byte[] {0x75, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49},
DarkPattern = new byte[] {0x74, 0x15, 0x33, 0xC0, 0xEB, 0x13, 0x90, 0x49}
}
};
private static readonly List<PatchInfo> MacPatches = new List<PatchInfo>
{
new PatchInfo
{
Version = "2018.4",
DarkPattern = new byte[] {0x75, 0x03, 0x41, 0x8B, 0x06, 0x4C, 0x3B},
LightPattern = new byte[] {0x74, 0x03, 0x41, 0x8B, 0x06, 0x4C, 0x3B}
},
new PatchInfo
{
Version = "2019.1",
DarkPattern = new byte[] {0x75, 0x03, 0x41, 0x8b, 0x06, 0x48},
LightPattern = new byte[] {0x74, 0x03, 0x41, 0x8b, 0x06, 0x48}
},
new PatchInfo
{
Version = "2019.2",
DarkPattern = new byte[] {0x75, 0x04, 0x8b, 0x03, 0xeb, 0x02},
LightPattern = new byte[] {0x74, 0x04, 0x8b, 0x03, 0xeb, 0x02}
},
new PatchInfo
{
Version = "2019.3",
DarkPattern = new byte[] {0x85, 0xD5, 0x00, 0x00, 0x00, 0x8B, 0x03},
LightPattern = new byte[] {0x84, 0xD5, 0x00, 0x00, 0x00, 0x8B, 0x03}
},
new PatchInfo
{
Version = "2019.4",
DarkPattern = new byte[] {0x85, 0xD5, 0x00, 0x00, 0x00, 0x8B, 0x03},
LightPattern = new byte[] {0x84, 0xD5, 0x00, 0x00, 0x00, 0x8B, 0x03}
},
new PatchInfo
{
Version = "2020.1",
DarkPattern = new byte[] {0x75, 0x5E, 0x8B, 0x03, 0xEB},
LightPattern = new byte[] {0x74, 0x5E, 0x8B, 0x03, 0xEB}
},
new PatchInfo
{
Version = "2020.2",
DarkPattern = new byte[] {0x75, 0x5E, 0x8B, 0x03, 0xEB},
LightPattern = new byte[] {0x74, 0x5E, 0x8B, 0x03, 0xEB}
}
};
private static readonly List<PatchInfo> LinuxPatches = new List<PatchInfo>
{
new PatchInfo
{
Version = "2018.4",
DarkPattern = new byte[] {0x75, 0x03, 0x41, 0x8b, 0x06, 0x48},
LightPattern = new byte[] {0x74, 0x03, 0x41, 0x8b, 0x06, 0x48}
},
new PatchInfo
{
Version = "2019.1",
DarkPattern = new byte[] {0x75, 0x04, 0x41, 0x8b, 0x45, 0x00, 0x43, 0x83},
LightPattern = new byte[] {0x74, 0x04, 0x41, 0x8b, 0x45, 0x00, 0x43, 0x83}
},
new PatchInfo
{
Version = "2019.2",
DarkPattern = new byte[] {0x75, 0x02, 0x8b, 0x03, 0x48, 0x83},
LightPattern = new byte[] {0x74, 0x02, 0x8b, 0x03, 0x48, 0x83}
},
new PatchInfo
{
Version = "2019.3",
DarkPattern = new byte[] {0x75, 0x06, 0x41, 0x8b, 0x04, 0x24, 0xeb, 0x02},
LightPattern = new byte[] {0x74, 0x06, 0x41, 0x8b, 0x04, 0x24, 0xeb, 0x02}
},
new PatchInfo
{
Version = "2019.4",
DarkPattern = new byte[] {0x75, 0x06, 0x41, 0x8b, 0x04, 0x24, 0xeb, 0x02},
LightPattern = new byte[] {0x74, 0x06, 0x41, 0x8b, 0x04, 0x24, 0xeb, 0x02}
},
new PatchInfo
{
Version = "2020.1",
DarkPattern = new byte[] {0x75, 0x05, 0x41, 0x8b, 0x07, 0xeb, 0x02},
LightPattern = new byte[] {0x74, 0x05, 0x41, 0x8b, 0x07, 0xeb, 0x02}
},
new PatchInfo
{
Version = "2020.2",
DarkPattern = new byte[] {0x75, 0x5e, 0x8b, 0x03, 0xeb, 0x5c, 0x48},
LightPattern = new byte[] {0x74, 0x5e, 0x8b, 0x03, 0xeb, 0x5c, 0x48}
}
};
public static List<PatchInfo> GetPatches(OperatingSystem os)
{
var patches = os switch
{
OperatingSystem.Windows => WindowsPatches,
OperatingSystem.MacOS => MacPatches,
OperatingSystem.Linux => LinuxPatches,
_ => throw new ArgumentOutOfRangeException(nameof(os))
};
return patches.OrderByDescending(info => info.Version).ToList();
}
}
}
================================================
FILE: src/Patcher/Program.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NDesk.Options;
using System.Runtime.InteropServices;
namespace Patcher
{
internal static class Program
{
internal static void Main(string[] args)
{
var themeName = string.Empty;
var help = false;
var fileLocation = string.Empty;
var os = OperatingSystem.Unknown;
var version = string.Empty;
var force = false;
var optionSet = new OptionSet
{
{"theme=|t=", "The theme to be applied to the Unity.", v => themeName = v},
{
"exe=|e=", "The location of the Unity Editor executable.",
v => fileLocation = v
},
{
"mac", "Specifies if the specified binary is the MacOS version of Unity3D.",
v =>
{
if (v != null) os = OperatingSystem.MacOS;
}
},
{
"linux", "Specifies if the specified binary is the Linux version of Unity3D.",
v =>
{
if (v != null) os = OperatingSystem.Linux;
}
},
{
"windows", "Specifies if the specified binary is the Windows version of Unity3D.",
v =>
{
if (v != null) os = OperatingSystem.Windows;
}
},
{
"version=|v=", "The version of Unity to patch.", v => version = v
},
{
"force|f", "Gently applies force.", v => force = v != null
},
{"help|h", v => help = v != null}
};
string error = null;
try
{
var leftover = optionSet.Parse(args);
if (leftover.Any())
error = "Unknown arguments: " + string.Join(" ", leftover);
}
catch (OptionException ex)
{
error = ex.Message;
}
if (error != null)
{
Console.WriteLine($"Unity Patcher: {error}");
Console.WriteLine("Use --help for a list of supported options.");
return;
}
if (string.IsNullOrEmpty(themeName))
themeName = "dark";
if (!themeName.Equals("dark", StringComparison.OrdinalIgnoreCase) &&
!themeName.Equals("light", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"Theme \"{themeName}\" is not supported - please specify either dark or light.");
return;
}
if (help)
{
Console.WriteLine("Usage: patcher.exe");
optionSet.WriteOptionDescriptions(Console.Out);
return;
}
if (os == OperatingSystem.Unknown)
{
os = DetectOperatingSystem();
if (os == OperatingSystem.Unknown)
{
Console.WriteLine(
"Failed to detect OS and non was specified - please specify a valid operating system. See patcher -h for available options.");
return;
}
}
var patches = Patches.GetPatches(os);
var patch = patches.FirstOrDefault(p => p.Version.Equals(version, StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrEmpty(fileLocation))
{
// https://docs.unity3d.com/Manual/GettingStartedInstallingHub.html
var unityInstallations = UnityInstallation.GetUnityInstallations(os);
Console.WriteLine("Please choose the editor which should get patched:");
UnityInstallation selectedInstallation = ConsoleUtility.ShowSelectionMenu(unityInstallations.ToList(),
installation =>
{
var supported = installation.IsSupported(patches) ? "Supported" : "Unsupported";
return $"{installation.Version}\t({supported})";
});
fileLocation = selectedInstallation.ExecutablePath();
version = selectedInstallation.Version;
patch ??= selectedInstallation.GetPatch(patches);
if (patch == null)
{
Console.WriteLine("Couldn't find Patch for specified Unity Installation, please choose one:");
patch = ConsoleUtility.ShowSelectionMenu(patches, patchInfo => patchInfo.Version);
}
}
if (patch == null)
{
patch = patches.First();
Console.WriteLine(string.IsNullOrWhiteSpace(version)
? $"Version not explicitly specified -- defaulting to version {patch.Version} for {os}"
: $"Could not find patch details for {os} Unity version {version} -- defaulting to version {patch.Version}.");
}
else
{
Console.WriteLine($"Applying Patch for {patch.Version}");
}
Console.WriteLine($"Opening Unity executable from {fileLocation}...");
try
{
var fileInfo = new FileInfo(fileLocation);
if (!fileInfo.Exists)
{
Console.WriteLine($"Could not find the specified file: {fileInfo.FullName}");
return;
}
if (fileInfo.IsReadOnly)
{
Console.WriteLine("Can not patch the specified file - it is marked as read only!");
return;
}
using var fs = File.Open(fileInfo.FullName, FileMode.Open, FileAccess.ReadWrite);
using var ms = new MemoryStream();
fs.CopyTo(ms);
CreateBackup(fileInfo, ms);
PatchExecutable(ms, fs, patch, themeName, force);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine(
"Could not open the Unity executable - are you running the patcher as an administrator?");
}
}
private static void CreateBackup(FileSystemInfo fileInfo, MemoryStream ms)
{
Console.WriteLine("Creating backup...");
var backupFileInfo = new FileInfo(fileInfo.FullName + ".bak");
if (backupFileInfo.Exists)
backupFileInfo.Delete();
using var backupWriteStream = backupFileInfo.OpenWrite();
backupWriteStream.Write(ms.ToArray(), 0, (int)ms.Length);
if (backupFileInfo.Exists)
Console.WriteLine($"Backup '{backupFileInfo.Name}' created.");
}
private static void PatchExecutable(MemoryStream ms, FileStream fs, PatchInfo patch, string themeName,
bool force)
{
Console.WriteLine("Searching for theme offset...");
var buffer = ms.ToArray();
var lightOffsets = FindPattern(patch.LightPattern, buffer).ToArray();
var darkOffsets = FindPattern(patch.DarkPattern, buffer).ToArray();
var offsets = new HashSet<int>(lightOffsets);
offsets.UnionWith(darkOffsets);
var found = offsets.Any();
if (!found)
{
Console.WriteLine("Error: Could not find the theme offset in the specified executable!");
return;
}
var foundMultipleOffsets = offsets.Count > 1;
if (foundMultipleOffsets)
{
Console.WriteLine(
$"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}");
Console.WriteLine(
"Run the patcher with the --force option if you want to patch regardless of this warning.");
if (!force)
return;
}
var themeBytes = themeName == "dark" ? patch.DarkPattern : patch.LightPattern;
Console.WriteLine($"Patching to {themeName}...");
foreach (var offset in offsets)
for (var i = 0; i < themeBytes.Length; i++)
{
fs.Position = offset + i;
fs.WriteByte(themeBytes[i]);
}
Console.WriteLine("Unity was successfully patched. Enjoy!");
}
private static IEnumerable<int> FindPattern(byte[] needle, byte[] haystack)
{
return new BinarySearcher(needle).Search(haystack).ToArray();
}
private static OperatingSystem DetectOperatingSystem()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return OperatingSystem.Windows;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return OperatingSystem.MacOS;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return OperatingSystem.Linux;
}
return OperatingSystem.Unknown;
}
}
}
================================================
FILE: src/Patcher/UnityInstallation.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace Patcher
{
/// <summary>
/// Represents an Unity Installation on the Disk.
/// </summary>
public class UnityInstallation
{
private readonly string _installationLocation;
private readonly OperatingSystem _operatingSystem;
/// <summary>
/// Creates a new Unity installation
/// </summary>
/// <param name="installationLocation">The Path to this specific installation on the File System</param>
/// <param name="operatingSystem"></param>
public UnityInstallation(string installationLocation, OperatingSystem operatingSystem)
{
_installationLocation = installationLocation ?? throw new ArgumentNullException(nameof(installationLocation));
_operatingSystem = operatingSystem;
}
/// <summary>
/// The Version of the Unity Installation
/// </summary>
public string Version => Path.GetFileName(_installationLocation);
/// <summary>
/// Gets the Path to the Unity executable on the disk.
/// </summary>
/// <returns></returns>
public string ExecutablePath() => Path.Combine(_installationLocation, _operatingSystem switch
{
OperatingSystem.Windows => @"Editor\Unity.exe",
OperatingSystem.MacOS => "Unity.app/Contents/MacOS/Unity",
OperatingSystem.Linux => "Unity",
_ => throw new ArgumentOutOfRangeException(nameof(_operatingSystem))
});
public bool IsSupported(IEnumerable<PatchInfo> patches)
{
return GetPatch(patches) != null;
}
public PatchInfo GetPatch(IEnumerable<PatchInfo> patches)
{
foreach (PatchInfo patch in patches)
{
if (Regex.IsMatch(Version, patch.Version))
{
return patch;
}
}
return null;
}
public static IEnumerable<UnityInstallation> GetUnityInstallations(OperatingSystem operatingSystem)
{
var path = operatingSystem switch
{
OperatingSystem.Windows => @"C:\Program Files\Unity\Hub\Editor\",
OperatingSystem.MacOS => "/Applications/Unity/Hub/Editor",
OperatingSystem.Linux => "~/Unity/Hub/Editor",
_ => throw new ArgumentOutOfRangeException(nameof(operatingSystem))
};
if (!Directory.Exists(path)) yield break;
var directories = Directory.GetDirectories(path);
Array.Sort(directories);
foreach (string directory in directories)
{
yield return new UnityInstallation(directory, operatingSystem);
}
}
}
}
================================================
FILE: src/Patcher.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28010.2041
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patcher", "Patcher\Patcher.csproj", "{9C373F82-C468-43A9-BCE7-2ADFF32D195C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9C373F82-C468-43A9-BCE7-2ADFF32D195C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9C373F82-C468-43A9-BCE7-2ADFF32D195C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C373F82-C468-43A9-BCE7-2ADFF32D195C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C373F82-C468-43A9-BCE7-2ADFF32D195C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {30DFC04F-53D2-4ACB-B3B1-EFB8FF0E2B3D}
EndGlobalSection
EndGlobal
================================================
FILE: tools/global.json
================================================
{
"sdk": {
"version": "3.1.201"
}
}
================================================
FILE: tools/publish.ps1
================================================
dotnet publish ../src/Patcher.sln -o="./publish/win-x64" -f="netcoreapp3.1" -p:PublishSingleFile=true -c=Release --runtime win-x64
dotnet publish ../src/Patcher.sln -o="./publish/osx-64" -f="netcoreapp3.1" -p:PublishSingleFile=true -c=Release --runtime osx-x64
dotnet publish ../src/Patcher.sln -o="./publish/linux-x64" -f="netcoreapp3.1" -p:PublishSingleFile=true -c=Release --runtime linux-x64
gitextract_jzgj9f8a/
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── incompatible-version.md
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── README.md
├── src/
│ ├── Patcher/
│ │ ├── BinarySearcher.cs
│ │ ├── ConsoleUtility.cs
│ │ ├── OperatingSystem.cs
│ │ ├── Options.cs
│ │ ├── PatchInfo.cs
│ │ ├── Patcher.csproj
│ │ ├── Patches.cs
│ │ ├── Program.cs
│ │ └── UnityInstallation.cs
│ └── Patcher.sln
└── tools/
├── global.json
└── publish.ps1
SYMBOL INDEX (113 symbols across 8 files)
FILE: src/Patcher/BinarySearcher.cs
class BinarySearcher (line 9) | public sealed class BinarySearcher
method BinarySearcher (line 15) | public BinarySearcher(byte[] needle)
method Search (line 22) | public IEnumerable<int> Search(byte[] haystack)
method makeByteTable (line 45) | static int[] makeByteTable(byte[] needle)
method makeOffsetTable (line 58) | static int[] makeOffsetTable(byte[] needle)
method isPrefix (line 80) | static bool isPrefix(byte[] needle, int p)
method suffixLength (line 89) | static int suffixLength(byte[] needle, int p)
FILE: src/Patcher/ConsoleUtility.cs
class ConsoleUtility (line 9) | public static class ConsoleUtility
method ShowSelectionMenu (line 21) | public static T ShowSelectionMenu<T>(List<T> options, Func<T, string> ...
FILE: src/Patcher/OperatingSystem.cs
type OperatingSystem (line 3) | public enum OperatingSystem
FILE: src/Patcher/Options.cs
class OptionValueCollection (line 149) | public class OptionValueCollection : IList, IList<string>
method OptionValueCollection (line 155) | internal OptionValueCollection(OptionContext c)
method CopyTo (line 161) | void ICollection.CopyTo(Array array, int index) { (values as ICollecti...
method Add (line 167) | public void Add(string item) { values.Add(item); }
method Clear (line 168) | public void Clear() { values.Clear(); }
method Contains (line 169) | public bool Contains(string item) { return values.Contains(item); }
method CopyTo (line 170) | public void CopyTo(string[] array, int arrayIndex) { values.CopyTo(arr...
method Remove (line 171) | public bool Remove(string item) { return values.Remove(item); }
method GetEnumerator (line 177) | IEnumerator IEnumerable.GetEnumerator() { return values.GetEnumerator(...
method GetEnumerator (line 181) | public IEnumerator<string> GetEnumerator() { return values.GetEnumerat...
method Add (line 185) | int IList.Add(object value) { return (values as IList).Add(value); }
method Contains (line 186) | bool IList.Contains(object value) { return (values as IList).Contains(...
method IndexOf (line 187) | int IList.IndexOf(object value) { return (values as IList).IndexOf(val...
method Insert (line 188) | void IList.Insert(int index, object value) { (values as IList).Insert(...
method Remove (line 189) | void IList.Remove(object value) { (values as IList).Remove(value); }
method RemoveAt (line 190) | void IList.RemoveAt(int index) { (values as IList).RemoveAt(index); }
method IndexOf (line 196) | public int IndexOf(string item) { return values.IndexOf(item); }
method Insert (line 197) | public void Insert(int index, string item) { values.Insert(index, item...
method RemoveAt (line 198) | public void RemoveAt(int index) { values.RemoveAt(index); }
method AssertValid (line 200) | private void AssertValid(int index)
method ToList (line 227) | public List<string> ToList()
method ToArray (line 232) | public string[] ToArray()
method ToString (line 237) | public override string ToString()
class OptionContext (line 243) | public class OptionContext
method OptionContext (line 251) | public OptionContext(OptionSet set)
type OptionValueType (line 286) | public enum OptionValueType
class Option (line 293) | public abstract class Option
method Option (line 301) | protected Option(string prototype, string description)
method Option (line 306) | protected Option(string prototype, string description, int maxValueCount)
method GetNames (line 343) | public string[] GetNames()
method GetValueSeparators (line 348) | public string[] GetValueSeparators()
method Parse (line 355) | protected static T Parse<T>(string value, OptionContext c)
method ParsePrototype (line 380) | private OptionValueType ParsePrototype()
method AddSeparators (line 423) | private static void AddSeparators(string name, int end, ICollection<st...
method Invoke (line 457) | public void Invoke(OptionContext c)
method OnParseComplete (line 465) | protected abstract void OnParseComplete(OptionContext c);
method ToString (line 467) | public override string ToString()
class OptionException (line 473) | [Serializable]
method OptionException (line 478) | public OptionException()
method OptionException (line 482) | public OptionException(string message, string optionName)
method OptionException (line 488) | public OptionException(string message, string optionName, Exception in...
method OptionException (line 494) | protected OptionException(SerializationInfo info, StreamingContext con...
method GetObjectData (line 505) | [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter ...
class OptionSet (line 515) | public class OptionSet : KeyedCollection<string, Option>
method OptionSet (line 517) | public OptionSet()
method OptionSet (line 522) | public OptionSet(Converter<string, string> localizer)
method GetKeyForItem (line 534) | protected override string GetKeyForItem(Option item)
method GetOptionForName (line 545) | [Obsolete("Use KeyedCollection.this[string]")]
method InsertItem (line 560) | protected override void InsertItem(int index, Option item)
method RemoveItem (line 566) | protected override void RemoveItem(int index)
method SetItem (line 577) | protected override void SetItem(int index, Option item)
method AddImpl (line 584) | private void AddImpl(Option option)
method Add (line 606) | public new OptionSet Add(Option option)
class ActionOption (line 612) | sealed class ActionOption : Option
method ActionOption (line 616) | public ActionOption(string prototype, string description, int count,...
method OnParseComplete (line 622) | protected override void OnParseComplete(OptionContext c)
method ActionOption (line 662) | public ActionOption(string prototype, string description, Action<T> ...
method OnParseComplete (line 668) | protected override void OnParseComplete(OptionContext c)
method ActionOption (line 678) | public ActionOption(string prototype, string description, OptionActi...
method OnParseComplete (line 684) | protected override void OnParseComplete(OptionContext c)
method Add (line 628) | public OptionSet Add(string prototype, Action<string> action)
method Add (line 633) | public OptionSet Add(string prototype, string description, Action<stri...
method Add (line 643) | public OptionSet Add(string prototype, OptionAction<string, string> ac...
method Add (line 648) | public OptionSet Add(string prototype, string description, OptionActio...
class ActionOption (line 658) | sealed class ActionOption<T> : Option
method ActionOption (line 616) | public ActionOption(string prototype, string description, int count,...
method OnParseComplete (line 622) | protected override void OnParseComplete(OptionContext c)
method ActionOption (line 662) | public ActionOption(string prototype, string description, Action<T> ...
method OnParseComplete (line 668) | protected override void OnParseComplete(OptionContext c)
method ActionOption (line 678) | public ActionOption(string prototype, string description, OptionActi...
method OnParseComplete (line 684) | protected override void OnParseComplete(OptionContext c)
class ActionOption (line 674) | sealed class ActionOption<TKey, TValue> : Option
method ActionOption (line 616) | public ActionOption(string prototype, string description, int count,...
method OnParseComplete (line 622) | protected override void OnParseComplete(OptionContext c)
method ActionOption (line 662) | public ActionOption(string prototype, string description, Action<T> ...
method OnParseComplete (line 668) | protected override void OnParseComplete(OptionContext c)
method ActionOption (line 678) | public ActionOption(string prototype, string description, OptionActi...
method OnParseComplete (line 684) | protected override void OnParseComplete(OptionContext c)
method Add (line 692) | public OptionSet Add<T>(string prototype, Action<T> action)
method Add (line 697) | public OptionSet Add<T>(string prototype, string description, Action<T...
method Add (line 702) | public OptionSet Add<TKey, TValue>(string prototype, OptionAction<TKey...
method Add (line 707) | public OptionSet Add<TKey, TValue>(string prototype, string descriptio...
method CreateOptionContext (line 712) | protected virtual OptionContext CreateOptionContext()
method Parse (line 718) | public List<string> Parse (IEnumerable<string> arguments)
method Parse (line 746) | public List<string> Parse(IEnumerable<string> arguments)
method Unprocessed (line 775) | private static bool Unprocessed(ICollection<string> extra, Option def,...
method GetOptionParts (line 791) | protected bool GetOptionParts(string argument, out string flag, out st...
method Parse (line 812) | protected virtual bool Parse(string argument, OptionContext c)
method ParseValue (line 852) | private void ParseValue(string option, OptionContext c)
method ParseBool (line 873) | private bool ParseBool(string option, string n, OptionContext c)
method ParseBundledValue (line 891) | private bool ParseBundledValue(string f, string n, OptionContext c)
method Invoke (line 929) | private static void Invoke(OptionContext c, string name, string value,...
method WriteOptionDescriptions (line 939) | public void WriteOptionDescriptions(TextWriter o)
method WriteOptionPrototype (line 966) | bool WriteOptionPrototype(TextWriter o, Option p, ref int written)
method GetNextOptionIndex (line 1016) | static int GetNextOptionIndex(string[] names, int i)
method Write (line 1025) | static void Write(TextWriter o, ref int n, string s)
method GetArgumentName (line 1031) | private static string GetArgumentName(int index, int maxIndex, string ...
method GetDescription (line 1057) | private static string GetDescription(string description)
method GetLines (line 1104) | private static List<string> GetLines(string description)
method GetLineEnd (line 1141) | private static int GetLineEnd(int start, int length, string description)
FILE: src/Patcher/PatchInfo.cs
class PatchInfo (line 3) | public class PatchInfo
FILE: src/Patcher/Patches.cs
class Patches (line 7) | public static class Patches
method GetPatches (line 147) | public static List<PatchInfo> GetPatches(OperatingSystem os)
FILE: src/Patcher/Program.cs
class Program (line 10) | internal static class Program
method Main (line 12) | internal static void Main(string[] args)
method CreateBackup (line 179) | private static void CreateBackup(FileSystemInfo fileInfo, MemoryStream...
method PatchExecutable (line 197) | private static void PatchExecutable(MemoryStream ms, FileStream fs, Pa...
method FindPattern (line 244) | private static IEnumerable<int> FindPattern(byte[] needle, byte[] hays...
method DetectOperatingSystem (line 249) | private static OperatingSystem DetectOperatingSystem()
FILE: src/Patcher/UnityInstallation.cs
class UnityInstallation (line 11) | public class UnityInstallation
method UnityInstallation (line 22) | public UnityInstallation(string installationLocation, OperatingSystem ...
method ExecutablePath (line 37) | public string ExecutablePath() => Path.Combine(_installationLocation, ...
method IsSupported (line 45) | public bool IsSupported(IEnumerable<PatchInfo> patches)
method GetPatch (line 50) | public PatchInfo GetPatch(IEnumerable<PatchInfo> patches)
method GetUnityInstallations (line 63) | public static IEnumerable<UnityInstallation> GetUnityInstallations(Ope...
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (83K chars).
[
{
"path": ".gitattributes",
"chars": 2518,
"preview": "###############################################################################\n# Set default behavior to automatically "
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 530,
"preview": "---\nname: Bug report\nabout: Report an issue with the patcher\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Description**\nA "
},
{
"path": ".github/ISSUE_TEMPLATE/incompatible-version.md",
"chars": 673,
"preview": "---\nname: Incompatible Version\nabout: Report an issue related to patcher compatibility\ntitle: ''\nlabels: ''\nassignees: '"
},
{
"path": ".github/workflows/ci.yml",
"chars": 291,
"preview": "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:"
},
{
"path": ".gitignore",
"chars": 4365,
"preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# User"
},
{
"path": "README.md",
"chars": 4351,
"preview": "\n\nUnity Patch\n===========\n\nThis repository co"
},
{
"path": "src/Patcher/BinarySearcher.cs",
"chars": 2737,
"preview": "using System;\nusing System.Collections.Generic;\n\nnamespace Patcher\n{\n /// <summary>\n /// Original by Matthew Watso"
},
{
"path": "src/Patcher/ConsoleUtility.cs",
"chars": 2262,
"preview": "using System;\nusing System.Collections.Generic;\n\nnamespace Patcher\n{\n /// <summary>\n /// Utility methods for inter"
},
{
"path": "src/Patcher/OperatingSystem.cs",
"chars": 129,
"preview": "namespace Patcher\n{\n public enum OperatingSystem\n {\n Unknown,\n Windows,\n MacOS,\n Linux"
},
{
"path": "src/Patcher/Options.cs",
"chars": 40492,
"preview": "//\n// Options.cs\n//\n// Authors:\n// Jonathan Pryor <jpryor@novell.com>\n//\n// Copyright (C) 2008 Novell (http://www.nove"
},
{
"path": "src/Patcher/PatchInfo.cs",
"chars": 204,
"preview": "namespace Patcher\n{\n public class PatchInfo\n {\n public string Version { get; set; }\n\n public byte[] "
},
{
"path": "src/Patcher/Patcher.csproj",
"chars": 291,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <OutputType>Exe</OutputType>\n <TargetFramework>netcoreapp3.1"
},
{
"path": "src/Patcher/Patches.cs",
"chars": 6437,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Patcher\n{\n public static class Patches\n"
},
{
"path": "src/Patcher/Program.cs",
"chars": 9772,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing NDesk.Options;\nusing System.Ru"
},
{
"path": "src/Patcher/UnityInstallation.cs",
"chars": 2901,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Text.RegularExpressions;\n\nnamespace Patche"
},
{
"path": "src/Patcher.sln",
"chars": 1093,
"preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio 15\nVisualStudioVersion = 15.0.28010.2041\nM"
},
{
"path": "tools/global.json",
"chars": 43,
"preview": "{\n \"sdk\": {\n \"version\": \"3.1.201\"\n }\n}"
},
{
"path": "tools/publish.ps1",
"chars": 396,
"preview": "dotnet publish ../src/Patcher.sln -o=\"./publish/win-x64\" -f=\"netcoreapp3.1\" -p:PublishSingleFile=true -c=Release --runt"
}
]
About this extraction
This page contains the full source code of the aevitas/unity-patch GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (77.6 KB), approximately 18.1k tokens, and a symbol index with 113 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.