diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.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 + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# 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 +# Note: 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 +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable 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 +*.appx +*.appxbundle +*.appxupload + +# 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 +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# 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 +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# 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/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/Config.cs b/Config.cs new file mode 100644 index 0000000..60bef72 --- /dev/null +++ b/Config.cs @@ -0,0 +1,68 @@ +using BepInEx.Configuration; + +namespace undead_universal_patch +{ + public static class GenericConfig + { + public static ConfigEntry DisableAmplitude; + public static ConfigEntry DisableGoogleAnalytics; + //public static ConfigEntry UnhideEditorRooms; + public static ConfigEntry LogAllRequests; + public static ConfigEntry IsNameserver; + } + public static class GenericConfigDefaults + { + public static bool DisableAmplitude = true; + public static bool DisableGoogleAnalytics = true; + //public static bool UnhideEditorRooms = false; + public static bool LogAllRequests = false; + public static bool IsNameserver = false; + } + public static class GameserverConfig + { + public static ConfigEntry UseGameserverHost; + public static ConfigEntry SecureProtocol; + public static ConfigEntry Host; + public static ConfigEntry UseSocketHost; + public static ConfigEntry SecureSocketProtocol; + public static ConfigEntry SocketHost; + } + + public static class GameserverConfigDefaults + { + public static bool UseGameserverHost = false; + public static bool SecureProtocol = true; + public static string Host = "nosuchsubdomain.google.com"; + public static bool UseSocketHost = false; + public static bool SecureSocketProtocol = true; + public static string SocketHost = "anothernonexistentsubdomain.google.com"; + } + public static class PhotonConfig + { + public static ConfigEntry PatchPhotonIds; + public static ConfigEntry AppID; + public static ConfigEntry VoiceAppID; + public static ConfigEntry PatchEvent; + } + public static class PhotonConfigDefaults + { + public static bool PatchPhotonIds = false; + public static string AppID = "replace-me-please"; + public static string VoiceAppID = "replace-me-please"; + public static string PatchEvent = "Load"; + } + public static class NameserverConfig + { + public static ConfigEntry UseNameserverHost; + public static ConfigEntry SecureProtocol; + public static ConfigEntry Host; + public static ConfigEntry Path; + } + public static class NameserverConfigDefaults + { + public static bool UseNameserverHost = false; + public static bool SecureProtocol = false; + public static string Host = "thissubdomaindoesnotexist.google.com"; + public static string Path = "/"; + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2cb54fd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +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. \ No newline at end of file diff --git a/Patches/BestHTTP.cs b/Patches/BestHTTP.cs new file mode 100644 index 0000000..3101703 --- /dev/null +++ b/Patches/BestHTTP.cs @@ -0,0 +1,133 @@ +using System; +using System.Reflection; +using HarmonyLib; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch] + class BestHTTP_MonoOne_Patch + { + static string TargetTypeName = "BestHTTP.EDGFEAGFEKH"; + static string TargetMethodName = "MMOHCNJHBLK"; + static string Description = "2018 BestHTTP request (early) rewrite patch"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + static readonly Type requestType = AccessTools.TypeByName("BestHTTP.KGBBPEDECCO"); + + static bool Prepare() + { + if (targetType == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); + return false; + } + MethodInfo requestMethod = AccessTools.Method(targetType, TargetMethodName, [ requestType ]); + if (requestMethod == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); + return false; + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + + static MethodBase TargetMethod() + { + return AccessTools.Method(targetType, TargetMethodName, [ requestType ]); + } + + [HarmonyPrefix] + static bool Prefix(ref object OCJJLJHMMGH) + { + PropertyInfo uriProperty = AccessTools.Property(requestType, "NJCAFJDHMHI"); + if (uriProperty == null) + { + Plugin.Log.LogFatal("BestHTTP_MonoOne failed: NJCAFJDHMHI was null."); + return false; + } + + var uriInstance = (Uri)uriProperty.GetValue(OCJJLJHMMGH, null); + if (uriInstance == null) + { + Plugin.Log.LogFatal("BestHTTP_MonoOne failed: uriInstance was null."); + return false; + } + + if (uriInstance.Host.Contains("api.amplitude.com") && GenericConfig.DisableAmplitude.Value) return false; + + Plugin.Log.LogInfo($"BestHTTP_MonoOne request (Before) URL: {uriInstance.ToString()}"); + + Uri newUri; + if (GenericConfig.IsNameserver.Value) newUri = PatchUtils.RewriteUrlForNameserver(uriInstance); + else newUri = PatchUtils.RewriteUrlForGameserver(uriInstance); + + Plugin.Log.LogInfo($"BestHTTP_MonoOne request (After) URL: {newUri.ToString()}"); + + uriProperty.SetValue(OCJJLJHMMGH, newUri, null); + return true; + } + } + [HarmonyPatch] + class BestHTTP_MonoTwo_Patch + { + static string TargetTypeName = "BestHTTP.IGMACIPKKLJ"; + static string TargetMethodName = "LIADPGACMDN"; + static string Description = "2018 BestHTTP request (late) rewrite patch"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + static readonly Type requestType = AccessTools.TypeByName("BestHTTP.CLFJEGPBDII"); + + static bool Prepare() + { + if (targetType == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); + return false; + } + MethodInfo requestMethod = AccessTools.Method(targetType, TargetMethodName, [requestType]); + if (requestMethod == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The method for this patch was not found."); + return false; + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + + static MethodBase TargetMethod() + { + return AccessTools.Method(targetType, TargetMethodName, [requestType]); + } + + [HarmonyPrefix] + static bool Prefix(ref object GFKPFLABKMA) + { + PropertyInfo uriProperty = AccessTools.Property(requestType, "ECJJELCCIJA"); + if (uriProperty == null) + { + Plugin.Log.LogFatal("BestHTTP_MonoTwo failed: ECJJELCCIJA was null."); + return false; + } + + var uriInstance = (Uri)uriProperty.GetValue(GFKPFLABKMA, null); + if (uriInstance == null) + { + Plugin.Log.LogFatal("BestHTTP_MonoTwo failed: uriInstance was null."); + return false; + } + + if (uriInstance.Host.Contains("api.amplitude.com") && GenericConfig.DisableAmplitude.Value) return false; + + Plugin.Log.LogInfo($"BestHTTP_MonoTwo request (Before) URL: {uriInstance.ToString()}"); + + Uri newUri; + if (GenericConfig.IsNameserver.Value) newUri = PatchUtils.RewriteUrlForNameserver(uriInstance); + else newUri = PatchUtils.RewriteUrlForGameserver(uriInstance); + + Plugin.Log.LogInfo($"BestHTTP_MonoTwo request (After) URL: {newUri.ToString()}"); + + uriProperty.SetValue(GFKPFLABKMA, newUri, null); + return true; + } + } +} diff --git a/Patches/LWebSocket.cs b/Patches/LWebSocket.cs new file mode 100644 index 0000000..7b97083 --- /dev/null +++ b/Patches/LWebSocket.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using HarmonyLib; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch] + class LWebSocket + { + static string TargetTypeName = "WebSocketSharp.WebSocket"; + static string Description = "Legacy WebSocket (websocket-sharp) URL rewrite patch"; + static Type webSocketType = AccessTools.TypeByName(TargetTypeName); + + static bool Prepare() + { + if (GenericConfig.IsNameserver.Value) return false; + if (webSocketType == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The type this patch targets was not found."); + return false; + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + + static MethodBase TargetMethod() + { + return AccessTools.Constructor(webSocketType, [ typeof(string), typeof(string[]) ]); + } + + [HarmonyPrefix] + static void Prefix(ref string url) + { + if (PhotonConfig.PatchEvent.Value == "SocketConnect" && PhotonConfig.PatchPhotonIds.Value) Photon.Patch(); + url = PatchUtils.RewriteUrlForSocket(url).ToString(); + } + } +} diff --git a/Patches/NWebSocket.cs b/Patches/NWebSocket.cs new file mode 100644 index 0000000..8f2f3bf --- /dev/null +++ b/Patches/NWebSocket.cs @@ -0,0 +1,39 @@ +using System; +using System.Reflection; +using HarmonyLib; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch] + class NWebSocket + { + static string TargetTypeName = "BestHTTP.WebSocket.OHKCAHPNEGE"; + static string Description = "Late 2018 WebSocket (non-signalr) URL rewrite patch"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + + static bool Prepare() + { + if (GenericConfig.IsNameserver.Value) return false; + if (targetType == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); + return false; + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + + static MethodBase TargetMethod() + { + return AccessTools.Constructor(targetType, [ typeof(Uri) ]); + } + + [HarmonyPrefix] + static void Prefix(ref Uri AOHFKOJPKDN) + { + if (PhotonConfig.PatchEvent.Value == "SocketConnect" && PhotonConfig.PatchPhotonIds.Value) Photon.Patch(); + AOHFKOJPKDN = PatchUtils.RewriteUrlForSocket(AOHFKOJPKDN); + } + } +} diff --git a/Patches/OWebSocket.cs b/Patches/OWebSocket.cs new file mode 100644 index 0000000..7ba7fe4 --- /dev/null +++ b/Patches/OWebSocket.cs @@ -0,0 +1,39 @@ +using System; +using System.Reflection; +using HarmonyLib; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch] + class OWebSocket + { + static string TargetTypeName = "BestHTTP.WebSocket.IJADBHMBMPG"; + static string Description = "2017/mid-2018 WebSocket (non-signalr) URL rewrite patch"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + + static bool Prepare() + { + if (GenericConfig.IsNameserver.Value) return false; + if (targetType == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); + return false; + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + + static MethodBase TargetMethod() + { + return AccessTools.Constructor(targetType, [ typeof(Uri) ]); + } + + [HarmonyPrefix] + static void Prefix(ref Uri CAGBGCGKOKD) + { + if (PhotonConfig.PatchEvent.Value == "SocketConnect" && PhotonConfig.PatchPhotonIds.Value) Photon.Patch(); + CAGBGCGKOKD = PatchUtils.RewriteUrlForSocket(CAGBGCGKOKD); + } + } +} diff --git a/Patches/Photon.cs b/Patches/Photon.cs new file mode 100644 index 0000000..82e7e55 --- /dev/null +++ b/Patches/Photon.cs @@ -0,0 +1,121 @@ +using HarmonyLib; +using System; +using UnityEngine; +using System.Reflection; +using System.Collections.Generic; + +namespace undead_universal_patch.Patches +{ + public class Photon + { + public static void Patch() + { + Plugin.Log.LogInfo("Attempting Photon patch."); + Type serverSettingsType = AccessTools.TypeByName("ServerSettings"); + + Type hostingOptionType = AccessTools.Inner(serverSettingsType, "HostingOption"); + if (serverSettingsType == null) + { + Plugin.Log.LogFatal("Photon patch failed early, this Photon client is unsupported!"); + return; + } + + string[] possibleNames = ["PHMMPAMMFLB", "PIHPJGJOPJA", "JDFMBDBEMCJ", "IECCGIPNGLF"]; + if (hostingOptionType == null) foreach (string type in possibleNames) + { + Plugin.Log.LogDebug($"Checking {type}"); + hostingOptionType = AccessTools.Inner(serverSettingsType, type); + if (hostingOptionType == null) continue; + else break; + } + if (hostingOptionType == null) + { + Plugin.Log.LogFatal("Photon patch failed early, this Photon client (HostingOption) is unsupported!"); + return; + } + ScriptableObject settingsInstance = ScriptableObject.CreateInstance(serverSettingsType); + + object realPhotonServerSettings = Resources.Load("PhotonServerSettings", serverSettingsType); + + try + { + var rpcListField = AccessTools.Field(serverSettingsType, "RpcList"); + if (rpcListField == null) + { + Plugin.Log.LogFatal("Photon patch failed (serverSettingsType did not have an RpcList), this Photon client is unsupported!"); + return; + } + if (realPhotonServerSettings == null) + { + Plugin.Log.LogFatal("Photon patch failed (existing photon settings was null, is the patch event set to 'Awake'?), this Photon client is unsupported!"); + return; + } + var existingRpcList = (List)rpcListField.GetValue(realPhotonServerSettings); + rpcListField.SetValue(settingsInstance, existingRpcList); + } + catch (Exception e) + { + Plugin.Log.LogFatal("Photon patch failed (RpcList), this Photon client is unsupported!"); + Plugin.Log.LogDebug(e); + return; + } + + var appIdField = AccessTools.Field(serverSettingsType, "AppID"); + appIdField.SetValue(settingsInstance, PhotonConfig.AppID.Value); + var voiceAppIdField = AccessTools.Field(serverSettingsType, "VoiceAppID"); + voiceAppIdField.SetValue(settingsInstance, PhotonConfig.VoiceAppID.Value); + + var hostTypeField = AccessTools.Field(serverSettingsType, "HostType"); + if (hostTypeField != null && hostingOptionType != null && hostingOptionType.IsEnum) + { + try + { + object enumValue = Enum.Parse(hostingOptionType, "PhotonCloud"); + hostTypeField.SetValue(settingsInstance, enumValue); + } + catch (ArgumentException ex) + { + Plugin.Log.LogFatal($"Photon patch failed, cannot set HostingOption: {ex.Message}"); + return; + } + } + + // Save to property + Type photonNetworkType = AccessTools.TypeByName("PhotonNetwork"); + if (photonNetworkType == null) + { + string[] possibleTypeNames = ["GEFAIHCLLBI", "IHFKMIAHBFP", "DAHGDBPKKAJ"]; + foreach (string possibleTypeName in possibleTypeNames) + { + photonNetworkType = AccessTools.TypeByName(possibleTypeName); + if (photonNetworkType == null) continue; + else break; + } + if (photonNetworkType == null) + { + Plugin.Log.LogFatal("Photon patch will not work (class not found). Is this build supported?"); + return; + } + } + FieldInfo photonServerSettingsField = photonNetworkType.GetField("PhotonServerSettings"); + if (photonServerSettingsField == null) + { + string[] possibleTypeNames = ["OCOJPLFBNOG", "PBKGCAGGOLJ", "FJHKNHIIABD"]; + foreach (string possibleTypeName in possibleTypeNames) + { + photonServerSettingsField = photonNetworkType.GetField(possibleTypeName); + if (photonServerSettingsField == null) continue; + else break; + } + if (photonServerSettingsField == null) + { + Plugin.Log.LogFatal("Photon patch will not work (property not found). Is this build supported?"); + return; + } + } + photonServerSettingsField.SetValue(serverSettingsType, settingsInstance); + + Plugin.Log.LogInfo("Photon patch was successful."); + } + } +} diff --git a/Patches/Resource.cs b/Patches/Resource.cs new file mode 100644 index 0000000..ef430a0 --- /dev/null +++ b/Patches/Resource.cs @@ -0,0 +1,22 @@ +using System; +using HarmonyLib; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch(typeof(UnityEngine.Resources), nameof(UnityEngine.Resources.Load), [ typeof(string), typeof(Type) ])] + class Resource + { + static bool Ran = false; + [HarmonyPostfix] + static void Postfix(ref string path) + { + //Plugin.Log.LogInfo($"Resource loading '{path}'"); + if (path.Contains("PhotonServerSettings") && PhotonConfig.PatchPhotonIds.Value && PhotonConfig.PatchEvent.Value == "Load") + { + if (Ran) return; // Photon.Patch calls Resources.Load, which may result in a loop if not handled properly + Ran = true; + Photon.Patch(); + } + } + } +} diff --git a/Patches/UWRPatch-Str-Str-Rx-Tx.cs b/Patches/UWRPatch-Str-Str-Rx-Tx.cs new file mode 100644 index 0000000..634a374 --- /dev/null +++ b/Patches/UWRPatch-Str-Str-Rx-Tx.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Reflection; +using System.Security.Policy; +using HarmonyLib; +using UnityEngine; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch] + class UWR_Str_Str_Rx_Tx_Patch + { + const string TargetTypeName = "UnityEngine.Networking.UnityWebRequest"; + const string UWRParams = "(Str-Str-Rx-Tx)"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + + static bool Prepare() => UWRPatchNoArg.Prepare(); + + static MethodBase TargetMethod() + { + Type rxType = AccessTools.TypeByName("UnityEngine.Networking.DownloadHandler"); + Type txType = AccessTools.TypeByName("UnityEngine.Networking.UploadHandler"); + return AccessTools.Constructor(targetType, [ typeof(string), typeof(string), rxType, txType ]); + } + + [HarmonyPostfix] + static void Postfix(object __instance) => UWRPatchNoArg.InPostfix(__instance, UWRParams); + } +} diff --git a/Patches/UWRPatch-Str-Str.cs b/Patches/UWRPatch-Str-Str.cs new file mode 100644 index 0000000..f3e2442 --- /dev/null +++ b/Patches/UWRPatch-Str-Str.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Reflection; +using System.Security.Policy; +using HarmonyLib; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch] + class UWR_Str_Str_Patch + { + const string TargetTypeName = "UnityEngine.Networking.UnityWebRequest"; + const string UWRParams = "(Str-Str)"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + + static bool Prepare() => UWRPatchNoArg.Prepare(); + + static MethodBase TargetMethod() + { + return AccessTools.Constructor(targetType, [ typeof(string), typeof(string) ]); + } + + [HarmonyPostfix] + static void Postfix(object __instance) => UWRPatchNoArg.InPostfix(__instance, UWRParams); + } +} diff --git a/Patches/UWRPatch-Str.cs b/Patches/UWRPatch-Str.cs new file mode 100644 index 0000000..31a381f --- /dev/null +++ b/Patches/UWRPatch-Str.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Reflection; +using System.Security.Policy; +using HarmonyLib; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch] + class UWR_Str_Patch + { + const string TargetTypeName = "UnityEngine.Networking.UnityWebRequest"; + const string UWRParams = "(Str)"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + + static bool Prepare() => UWRPatchNoArg.Prepare(); + + static MethodBase TargetMethod() + { + return AccessTools.Constructor(targetType, [ typeof(string) ]); + } + + [HarmonyPrefix] + public static bool Prefix(ref string url) + { + if (url.Contains("api.amplitude.com") && GenericConfig.DisableAmplitude.Value) return false; + else return true; + } + + [HarmonyPostfix] + static void Postfix(object __instance) => UWRPatchNoArg.InPostfix(__instance, UWRParams); + } +} diff --git a/Patches/UWRPatchNoArg.cs b/Patches/UWRPatchNoArg.cs new file mode 100644 index 0000000..e032da9 --- /dev/null +++ b/Patches/UWRPatchNoArg.cs @@ -0,0 +1,58 @@ +using System; +using System.Reflection; +using HarmonyLib; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch] + class UWRPatchNoArg + { + const string TargetTypeName = "UnityEngine.Networking.UnityWebRequest"; + const string Description = "Legacy UnityWebRequest URL rewrite patch"; + const string UWRParams = "(NoArgs)"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + + public static bool Prepare() + { + if (GenericConfig.IsNameserver.Value) return false; + if (targetType == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); + return false; + } + if (GenericConfig.IsNameserver.Value) + { + Plugin.Log.LogInfo($"'{Description}' disabled. GenericConfig.IsNameserver was set."); + return false; + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + + static MethodBase TargetMethod() + { + return AccessTools.Constructor(targetType, []); + } + + [HarmonyPostfix] + static void Postfix(object __instance) => InPostfix(__instance, UWRParams); + + public static void InPostfix(object __instance, string UWRParams) + { + var urlProperty = AccessTools.Property(__instance.GetType(), "url"); + if (urlProperty == null) + { + Plugin.Log.LogWarning($"UWR {UWRParams} Prefix could not find the url field!"); + return; + } + + var targetUrl = (string)urlProperty.GetValue(__instance, null); + if (GenericConfig.LogAllRequests.Value) Plugin.Log.LogDebug($"UWR {UWRParams} Request URL: {targetUrl}"); + + urlProperty.SetValue(__instance, PatchUtils.RewriteUrlForGameserver(targetUrl).ToString(), null); + targetUrl = (string)urlProperty.GetValue(__instance, null); + if (GenericConfig.LogAllRequests.Value) Plugin.Log.LogDebug($"UWR {UWRParams} Request URL (Rewritten): {targetUrl}"); + } + } +} diff --git a/Patches/WWWPatch.cs b/Patches/WWWPatch.cs new file mode 100644 index 0000000..d21ce17 --- /dev/null +++ b/Patches/WWWPatch.cs @@ -0,0 +1,56 @@ +using System; +using System.Reflection; +using HarmonyLib; + +namespace undead_universal_patch.Patches +{ + [HarmonyPatch] + class WWWPatch + { + const string TargetTypeName = "UnityEngine.WWW"; + const string TargetMethodName = "InitWWW"; + const string Description = "Legacy WWW patch"; + static readonly Type targetType = AccessTools.TypeByName(TargetTypeName); + + static bool Prepare() + { + if (GenericConfig.IsNameserver.Value) return false; + if (targetType == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. The type for this patch was not found."); + return false; + } + if (TargetMethodName != null) + { + MethodInfo targetMethod = AccessTools.Method(targetType, TargetMethodName); + if (targetMethod == null) + { + Plugin.Log.LogWarning($"'{Description}' disabled. ({TargetTypeName}.{TargetMethodName} not found)"); + return false; + } + } + + Plugin.Log.LogInfo($"'{Description}' succeeded validation."); + return true; + } + + static MethodBase TargetMethod() + { + return AccessTools.Method(targetType, TargetMethodName, [ typeof(string), typeof(byte[]), typeof(string[]) ]); + } + + [HarmonyPrefix] + static bool Prefix(ref string url) + { + if (url == "http://www.againstgrav.com/motd") + { + url = $"{ConfigUtils.GetGameserverBaseUrl()}/api/legacyMoTD/v1"; + Plugin.Log.LogInfo("Redirected legacy MoTD."); + } + if (GenericConfig.DisableGoogleAnalytics.Value && url == "http://www.google-analytics.com/collect") return false; + + if (GenericConfig.LogAllRequests.Value) Plugin.Log.LogDebug($"Request URL: {url}"); + return true; + } + } +} diff --git a/Plugin.cs b/Plugin.cs new file mode 100644 index 0000000..ea43ebc --- /dev/null +++ b/Plugin.cs @@ -0,0 +1,101 @@ +using BepInEx; +using BepInEx.Logging; +using BepInEx.Unity.Mono; +using HarmonyLib; +using undead_universal_patch.Patches; + +namespace undead_universal_patch; + +[BepInPlugin("dev.proxnet.recroom.universalpatch.noneac.mono", "Undead Universal Patch", "1.0.0")] +public class Plugin : BaseUnityPlugin +{ + public static readonly ManualLogSource Log = BepInEx.Logging.Logger.CreateLogSource("UUPatch"); + + Harmony _hi = new("dev.proxnet.recroom.universalpatch.noneac.mono"); + + private void OnDestroy() + { + Log.LogInfo("Destroying."); + _hi.UnpatchSelf(); + } + + private void Awake() + { + // CONFIGURATION + + GameserverConfig.UseGameserverHost = Config.Bind("Gameserver", "UseGameserverHost", GameserverConfigDefaults.UseGameserverHost, + "Whether to rewrite outbound HTTP requests with our host or not."); + GameserverConfig.SecureProtocol = Config.Bind("Gameserver", "SecureProtocol", GameserverConfigDefaults.SecureProtocol, + "Whether to use 'http' or 'https' for our gameserver host." + + "\nUsed by legacy MoTD patch as well as the gameserver patch."); + GameserverConfig.Host = Config.Bind("Gameserver", "Host", GameserverConfigDefaults.Host, + "The host to use for our gameserver." + + "\nExamples include 'server.example.com', 'test.example.com:3939', and '127.0.2.5:19502'." + + "\nUsed by legacy MoTD patch as well as the gameserver patch."); + GameserverConfig.UseSocketHost = Config.Bind("Gameserver", "UseSocketHost", GameserverConfigDefaults.UseSocketHost, + "Flag that controls the usage of SocketHost and SecureSocketProtocol." + + "\nWhen enabled, the WebSocket URL will be modified to use the SocketHost and" + + "\nthe appropriate protocol ('wss' or 'ws')." + + "\nWhen disabled, the WebSocket URL will use the same host and secure status as the web server." + + "\nPaths are completely unchanged."); + GameserverConfig.SecureSocketProtocol = Config.Bind("Gameserver", "SecureSocketProtocol", GameserverConfigDefaults.SecureSocketProtocol, + "The secure protocol status to use ('wss' or 'ws')."); + GameserverConfig.SocketHost = Config.Bind("Gameserver", "SocketHost", GameserverConfigDefaults.SocketHost, + "The WebSocket host to use."); + GenericConfig.DisableAmplitude = Config.Bind("Generic", "DisableAmplitude", GenericConfigDefaults.DisableAmplitude, + "Many builds use Amplitude to log analytics." + + "\nYou can prevent the game from sending analytics with this bool."); + GenericConfig.DisableGoogleAnalytics = Config.Bind("Generic", "DisableGoogleAnalytics", GenericConfigDefaults.DisableGoogleAnalytics, + "Some legacy builds use Google Analytics rather than Amplitude." + + "\nYou can prevent the game from sending analytics with this bool." + + "\nThe WWW rewrite patch must validate for this to work."); + /*GenericConfig.UnhideEditorRooms = Config.Bind("Generic", "UnhideEditorRooms", GenericConfigDefaults.UnhideEditorRooms, + "Some rooms may have the flag 'IsEditorOnly' set, which prevents you from switching to that room." + + "\nUse this flag to always set this value to 'false', allowing you to switch to any internal activity.");*/ + GenericConfig.IsNameserver = Config.Bind("Generic", "IsNameserver", GenericConfigDefaults.IsNameserver, "" + + "Enable if you use a nameserver. This will disable URL rewriting for all WebSocket, WWW, and UWR patches."); + GenericConfig.LogAllRequests = Config.Bind("Generic", "LogAllRequests", GenericConfigDefaults.LogAllRequests, + "Log all HTTP requests sent by the game. Applies to all HTTP patches," + + "\nincluding WWWPatch, UWRPatch, and BestHTTPPatch."); + PhotonConfig.PatchPhotonIds = Config.Bind("Photon", "PatchPhotonIds", PhotonConfigDefaults.PatchPhotonIds, + "Enable/disable changing the target IDs in PhotonServerSettings." + + "\nCustom server settings are not yet supported."); + PhotonConfig.AppID = Config.Bind("Photon", "AppID", PhotonConfigDefaults.AppID, + "The new target (PUN) App ID from the Photon dashboard."); + PhotonConfig.VoiceAppID = Config.Bind("Photon", "VoiceAppID", PhotonConfigDefaults.VoiceAppID, + "The new target Voice App ID from the Photon dashboard."); + PhotonConfig.PatchEvent = Config.Bind("Photon", "PatchEvent", PhotonConfigDefaults.PatchEvent, + "The event on which to patch Photon. You may need to patch Photon" + + "\nat different times, depending on your build's PhotonServerSettings implementation." + + "\nIf your log throws errors about custom authentication related to Photon, and you know" + + "\nthat your Photon IDs are specified above (and the patch is enabled)," + + "\nor you get Photon patch errors, then change this to one of these values:" + + "\n'Awake': When this plugin loads." + + "\n'SocketConnect': When the WebSocket connects." + + "\n'Load': When PhotonNetwork loads PhotonServerSettings. (recommended)"); + NameserverConfig.UseNameserverHost = Config.Bind("Nameserver", "UseNameserverHost", NameserverConfigDefaults.UseNameserverHost, + "Whether to rewrite outbound HTTP requests to 'ns.rec.net' to our host or not." + + "\nYou must enable GenericConfig.IsNameserver as well."); + NameserverConfig.SecureProtocol = Config.Bind("Nameserver", "SecureProtocol", NameserverConfigDefaults.SecureProtocol, + "Whether to use 'http' or 'https' for our nameserver host."); + NameserverConfig.Host = Config.Bind("Nameserver", "Host", NameserverConfigDefaults.Host, + "The host to use for our nameserver." + + "\nYou can specify a custom path for the request with NameserverConfig.Path." + + "\nExamples of a host include 'server.example.com', 'test.example.com:3939', and '127.0.2.5:19502'."); + NameserverConfig.Path = Config.Bind("Nameserver", "Path", NameserverConfigDefaults.Path, + "The path to use for the nameserver request." + + "\nExamples include '/ns/2019', '/ns', '/nameserver'. Query cannot be specified here."); + + // END CONFIGURATION + + Log.LogInfo("Finding appropriate patches ..."); + // Run all applicable patches + _hi.PatchAll(); + Log.LogInfo("PATCH LIST START ==========="); + foreach (var method in _hi.GetPatchedMethods()) Log.LogInfo($"- {method.ToString()}"); + Log.LogInfo("PATCH LIST END ==========="); + //GenericUtils.PrintAllNamespaces(); + + if (PhotonConfig.PatchEvent.Value == "Awake" && PhotonConfig.PatchPhotonIds.Value) Photon.Patch(); + } +} diff --git a/Utils.cs b/Utils.cs new file mode 100644 index 0000000..88bbd72 --- /dev/null +++ b/Utils.cs @@ -0,0 +1,166 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace undead_universal_patch +{ + public static class ConfigUtils + { + public static string GetGameserverProtocol() + { + return GameserverConfig.SecureProtocol.Value ? "https" : "http"; + } + public static string GetGameserverSocketProtocol() + { + return GameserverConfig.SecureSocketProtocol.Value ? "wss" : "ws"; + } + + public static string GetGameserverBaseUrl() + { + string protocol = GetGameserverProtocol(); + return $"{protocol}://{GameserverConfig.Host.Value}"; + } + public static string GetGameserverSocketBaseUrl() + { + string protocol = GameserverConfig.SecureProtocol.Value ? "wss" : "ws"; + if (GameserverConfig.UseSocketHost.Value) return $"{protocol}://{GameserverConfig.SocketHost.Value}"; + else return $"{GetGameserverProtocol()}://${GameserverConfig.Host.Value}"; + } + public static string GetNameserverProtocol() + { + return NameserverConfig.SecureProtocol.Value ? "https" : "http"; + } + + } + public static class GenericUtils + { + public static void PrintAllNamespaces() + { + // ChatGPT GPT-4o: Is it possible to list all namespaces available in the context? + + /* + * Response: + * Listing all namespaces directly isn't straightforward because .NET and C# don't have a built-in method to enumerate all namespaces. + * However, you can list all types within the currently loaded assemblies and then deduce the namespaces from these types. + */ + + HashSet namespaces = []; + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + foreach (var type in assembly.GetTypes()) + if (!string.IsNullOrEmpty(type.Namespace)) namespaces.Add(type.Namespace); + } + catch (ReflectionTypeLoadException ex) + { + // Handle the case where some types cannot be loaded. + foreach (var type in ex.Types) + if (type != null && !string.IsNullOrEmpty(type.Namespace)) namespaces.Add(type.Namespace); + } + } + + Plugin.Log.LogDebug("====================="); + Plugin.Log.LogDebug($"Available namespaces:"); + foreach (var ns in namespaces.OrderBy(ns => ns)) + Plugin.Log.LogDebug($"- '{ns}'"); + Plugin.Log.LogDebug("====================="); + } + public static Uri RewriteUri(string originalUrl, string newHost, bool secure, string path, string scheme) + { + return RewriteUri(new Uri(originalUrl), newHost, secure, path, scheme); + } + public static Uri RewriteUri(Uri original, string newHost, bool secure, string path, string scheme) + { + UriBuilder newUri = new(original); + + string host; + int port; + if (newHost.Contains(":")) + { + string[] host_port = newHost.Split(':'); + if (host_port.Length > 2) + { + Plugin.Log.LogFatal("Could not rewrite URL: The new target host was not valid!"); + return null; + } + host = host_port[0]; + port = int.Parse(host_port[1]); + } + else + { + host = newHost; + if (secure) port = 443; + else port = 80; + } + + newUri.Host = host; + newUri.Path = path; + newUri.Port = port; + newUri.Scheme = scheme; + + return newUri.Uri; + } + } + public static class PatchUtils + { + public static Uri RewriteUrlForNameserver(Uri targetUri) + { + if (targetUri.Host.Contains("ns.rec.net") && NameserverConfig.UseNameserverHost.Value) + { + return GenericUtils.RewriteUri(targetUri, NameserverConfig.Host.Value, + NameserverConfig.SecureProtocol.Value, NameserverConfig.Path.Value, + ConfigUtils.GetNameserverProtocol()); + } + return targetUri; + } + public static Uri RewriteUrlForGameserver(Uri targetUri) + { + UriBuilder parsedUrl = new(targetUri); + + // Rewrites + if (GameserverConfig.UseGameserverHost.Value && + (parsedUrl.Host.Contains("azurewebsites.net") || parsedUrl.Host.Contains("againstgrav.com"))) + { + return GenericUtils.RewriteUri(targetUri, GameserverConfig.Host.Value, + GameserverConfig.SecureProtocol.Value, targetUri.AbsolutePath, + ConfigUtils.GetGameserverProtocol()); + } + + return parsedUrl.Uri; + } + public static Uri RewriteUrlForGameserver(string targetUrl) + { + return RewriteUrlForGameserver(new Uri(targetUrl)); + } + public static Uri RewriteUrlForSocket(string targetUrl) + { + return RewriteUrlForSocket(new Uri(targetUrl)); + } + public static Uri RewriteUrlForSocket(Uri targetUri) + { + UriBuilder parsedUrl = new(targetUri); + + string SocketHost = GameserverConfig.Host.Value; + string SocketScheme = ConfigUtils.GetGameserverSocketProtocol(); + bool SocketSecure = GameserverConfig.SecureProtocol.Value; + if (GameserverConfig.UseSocketHost.Value) + { + SocketHost = GameserverConfig.SocketHost.Value; + SocketSecure = GameserverConfig.SecureSocketProtocol.Value; + } + + // Rewrites + if (parsedUrl.Path.Contains("api/notification/v2") && !GenericConfig.IsNameserver.Value) + { + return GenericUtils.RewriteUri(targetUri, SocketHost, SocketSecure, targetUri.AbsolutePath, SocketScheme); + } + + return targetUri; + } + } +} diff --git a/undead-universal-patch-mono.csproj b/undead-universal-patch-mono.csproj new file mode 100644 index 0000000..0c9cdbe --- /dev/null +++ b/undead-universal-patch-mono.csproj @@ -0,0 +1,33 @@ + + + + net35 + undead_universal_patch_mono + Non-EAC, Mono build patcher for Rec Room (*2016*-*Dec-2018) + 1.0.0 + true + latest + + https://api.nuget.org/v3/index.json; + https://nuget.bepinex.dev/v3/index.json; + https://nuget.samboy.dev/v3/index.json + + undead_universal_patch_mono + + + + + + + + + + + + + + + G:\rr\Cyberjunk-Build-Test\BepInEx\core\0Harmony.dll + + + diff --git a/undead-universal-patch-mono.sln b/undead-universal-patch-mono.sln new file mode 100644 index 0000000..228fc57 --- /dev/null +++ b/undead-universal-patch-mono.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "undead-universal-patch-mono", "undead-universal-patch-mono.csproj", "{CEFCB087-936C-4093-90C8-4CC1556AC0EA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CEFCB087-936C-4093-90C8-4CC1556AC0EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CEFCB087-936C-4093-90C8-4CC1556AC0EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CEFCB087-936C-4093-90C8-4CC1556AC0EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CEFCB087-936C-4093-90C8-4CC1556AC0EA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {133E4ED8-5BBF-4867-8726-82CB35557FE4} + EndGlobalSection +EndGlobal