public void RemovePatch() { try { PurgeFiles(); string[] entryPoint = m_strEntryPoint.Split(new string[] { "::" }, StringSplitOptions.None); string strTempFile = m_strGameAssemblyPath + Constants.VORTEX_BACKUP_TAG; File.Copy(m_strGameAssemblyPath, strTempFile, true); using (AssemblyDefinition unityAssembly = AssemblyDefinition.ReadAssembly(strTempFile, new ReaderParameters { ReadWrite = true })) { if (!IsInjected(unityAssembly, entryPoint)) { return; } TypeDefinition type = unityAssembly.MainModule.GetType(entryPoint[0]); if ((type == null) || !type.IsClass) { throw new EntryPointNotFoundException("Invalid type"); } MethodDefinition methodDefinition = type.Methods.FirstOrDefault(meth => meth.Name == entryPoint[1]); if ((methodDefinition == null) || !methodDefinition.HasBody) { throw new EntryPointNotFoundException("Invalid method"); } var instructions = methodDefinition.Body.Instructions .Where(instr => instr.OpCode == OpCodes.Call) .ToArray(); Instruction patcherInstr = instructions.FirstOrDefault(instr => instr.Operand.ToString().Contains(Constants.VORTEX_PATCH_METHOD)); methodDefinition.Body.Instructions.Remove(patcherInstr); unityAssembly.Write(m_strGameAssemblyPath); } } catch (Exception exc) { Enums.EErrorCode errorCode = Enums.EErrorCode.UNKNOWN; Util.RestoreBackup(m_strGameAssemblyPath); if (exc is FileNotFoundException) { errorCode = Enums.EErrorCode.MISSING_FILE; } else if (exc is EntryPointNotFoundException) { errorCode = Enums.EErrorCode.INVALID_ENTRYPOINT; } string strMessage = "Failed to remove patcher."; string strResponse = JSONResponse.CreateSerializedResponse(strMessage, errorCode, exc); Console.Error.WriteLine(strResponse); Environment.Exit((int)(errorCode)); } }
internal static string CreateSerializedResponse(string message, dynamic code, Exception exc = null) { JSONResponse response = new JSONResponse(); response.Message = message; response.ErrorCode = (int)(code); response.RaisedException = exc; return(JsonConvert.SerializeObject(response)); }
internal static void DeleteTemp(string strFilePath) { try { File.Delete(strFilePath); } catch (Exception exc) { string strMessage = "Failed to delete temporary file"; string strResponse = JSONResponse.CreateSerializedResponse(strMessage, Enums.EErrorCode.UNKNOWN, exc); Console.Error.WriteLine(strResponse); } }
internal static void ReplaceFile(string strOld, string strNew) { try { BackupFile(strOld, true); File.Delete(strOld); File.Copy(strNew, strOld); } catch (Exception exc) { RestoreBackup(strOld); string strMessage = "Failed to replace file"; string strResponse = JSONResponse.CreateSerializedResponse(strMessage, Enums.EErrorCode.FILE_OPERATION_ERROR, exc); Console.Error.WriteLine(strResponse); } }
/// <summary> /// Certain games distribute modified assemblies which disable /// reflection and impede modding. /// </summary> private void EnableReflection(string strDataPath) { string strMscorlib = Path.Combine(strDataPath, Constants.MSCORLIB); FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(strMscorlib); string version = fvi.FileVersion; string strLib = LIB_REPLACEMENTS .Where(replacement => replacement.Substring(Constants.MSCORLIB.Length + 1, 1) == version.Substring(0, 1)) .SingleOrDefault(); if (null != strLib) { try { WebClient wc = new WebClient(); Uri uri = new Uri(Constants.GITHUB_LINK + strLib); wc.DownloadDataAsync(uri, Path.Combine(strDataPath, strLib)); wc.DownloadDataCompleted += (sender, e) => { string strFileName = e.UserState.ToString(); File.WriteAllBytes(strFileName, e.Result); Util.ReplaceFile(strMscorlib, strFileName); m_eInjectorState = Enums.EInjectorState.FINISHED; }; } catch (Exception exc) { m_eInjectorState = Enums.EInjectorState.FINISHED; string strMessage = "Unhandled mscorlib version."; Enums.EErrorCode err = Enums.EErrorCode.MISSING_FILE; string strResponse = JSONResponse.CreateSerializedResponse(strMessage, err, exc); Console.Error.WriteLine(strResponse); Environment.Exit((int)(err)); } } else { m_eInjectorState = Enums.EInjectorState.FINISHED; string strMessage = "Unhandled mscorlib version."; Enums.EErrorCode err = Enums.EErrorCode.UNHANDLED_FILE_VERSION; string strResponse = JSONResponse.CreateSerializedResponse(strMessage, err); Console.Error.WriteLine(strResponse); Environment.Exit((int)(err)); } }
public Injector(string strDataPath, string strEntryPoint) { try { m_strDataPath = strDataPath; m_strEntryPoint = strEntryPoint; m_strGameAssemblyPath = Path.Combine(strDataPath, Constants.UNITY_ASSEMBLY_LIB); m_strModsDirectory = Path.Combine(strDataPath, Constants.MODS_DIRNAME); m_resolver = new MissingAssemblyResolver(strDataPath); } catch (Exception exc) { Enums.EErrorCode errorCode = Enums.EErrorCode.INVALID_ARGUMENT; string strMessage = "Injector received invalid argument."; string strResponse = JSONResponse.CreateSerializedResponse(strMessage, errorCode, exc); Console.Error.WriteLine(strResponse); Environment.Exit((int)(errorCode)); } }
public void Inject() { m_eInjectorState = Enums.EInjectorState.RUNNING; string strTempFile = null; AssemblyDefinition unityAssembly = null; try { // Ensure we have reflection enabled - there's no point // in continuing if reflection is disabled. if (!Util.IsReflectionEnabled(m_strDataPath)) { EnableReflection(m_strDataPath); } else { m_eInjectorState = Enums.EInjectorState.FINISHED; } // Deploy patcher related files. DeployFiles(); // Start the patching process. string[] patcherPoints = Constants.VORTEX_PATCH_METHOD.Split(new string[] { "::" }, StringSplitOptions.None); string[] entryPoint = m_strEntryPoint.Split(new string[] { "::" }, StringSplitOptions.None); strTempFile = Util.GetTempFile(m_strGameAssemblyPath); using (unityAssembly = AssemblyDefinition.ReadAssembly(strTempFile, new ReaderParameters { ReadWrite = true, AssemblyResolver = m_resolver })) { if (IsInjected(unityAssembly, entryPoint)) { unityAssembly.Dispose(); Util.DeleteTemp(strTempFile); return; } // Back up the game assembly before we do anything. Util.BackupFile(m_strGameAssemblyPath); AssemblyDefinition vrtxPatcher = AssemblyDefinition.ReadAssembly(Path.Combine(m_strDataPath, Constants.VORTEX_LIB)); MethodDefinition patcherMethod = vrtxPatcher.MainModule.GetType(patcherPoints[0]).Methods.First(x => x.Name == patcherPoints[1]); TypeDefinition type = unityAssembly.MainModule.GetType(entryPoint[0]); if ((type == null) || !type.IsClass) { throw new EntryPointNotFoundException("Invalid entry point"); } MethodDefinition methodDefinition = type.Methods.FirstOrDefault(meth => meth.Name == entryPoint[1]); if ((methodDefinition == null) || !methodDefinition.HasBody) { throw new EntryPointNotFoundException("Invalid entry point"); } methodDefinition.Body.GetILProcessor().InsertBefore(methodDefinition.Body.Instructions[0], Instruction.Create(OpCodes.Call, methodDefinition.Module.ImportReference(patcherMethod))); unityAssembly.Write(m_strGameAssemblyPath); unityAssembly.Dispose(); Util.DeleteTemp(strTempFile); } } catch (Exception exc) { Enums.EErrorCode errorCode = Enums.EErrorCode.UNKNOWN; if (unityAssembly != null) { unityAssembly.Dispose(); } if (strTempFile != null) { Util.DeleteTemp(strTempFile); } Util.RestoreBackup(m_strGameAssemblyPath); if (exc is FileNotFoundException) { errorCode = Enums.EErrorCode.MISSING_FILE; } else if (exc is EntryPointNotFoundException) { errorCode = Enums.EErrorCode.INVALID_ENTRYPOINT; } string strMessage = "Failed to inject patcher."; string strResponse = JSONResponse.CreateSerializedResponse(strMessage, errorCode, exc); Console.Error.WriteLine(strResponse); Environment.Exit((int)(errorCode)); } while (InjectorState != Enums.EInjectorState.FINISHED) { // Do nothing. } }
static void RunOptions(string [] args) { bool removePatch = false; bool showHelp = false; // (The -q argument should be used on its own as all other arguments will be ignored) // string will hold the path to the assembly we want to check for the .NET version. // if an absolute path is not provided, Vortex will assume that we're looking for Unity's // default assembly (Assembly-CSharp.dll) // This is currently used by Vortex to ascertain which .NET version we want to use when // building VIGO. string queryNETAssembly = string.Empty; // (This -a should be used on its own as all other arguments will be ignored) // string will hold both the path to the assembly we want to load and the // assembly name reference we want to verify. Value should have the following // format: "{Assembly_Path}::{Assembly_Name}" e.g. "C:/somePath/assembly.dll::mscorlib" string queryAssemblyName = string.Empty; OptionSet options = new OptionSet() .Add("h", "Shows this message and closes program", h => showHelp = h != null) .Add("g|extension=", "Path to the game's extension folder", g => m_strExtensionPath = g) .Add("m|managed=", "Path to the game's managed folder/game assembly", m => m_dataPath = m) .Add("i|install=", "Path to Harmony Patcher's build folder.", i => m_installPath = i) .Add("e|entry=", "This game's entry point formatted as: 'Namespace.ClassName::MethodName'", e => m_entryPoint = e) .Add("x|modsfolder=", "The game's expected mods directory", x => m_modsfolder = x) .Add("r", "Will remove the harmony patcher", r => removePatch = r != null) .Add("q|querynet=", "(optional) Query the .NET version of the assembly file we attempt to patch", q => queryNETAssembly = q) .Add("v", "(optional) Used to decide whether we want to use VIGO or not", v => m_injectVIGO = v != null) .Add("a|queryassemblyname=", "(optional) Used to check assembly references. Expected format: 'Assembly_Path::Assembly_Name'", a => queryAssemblyName = a); List <string> extra; try { extra = options.Parse(args); } catch (OptionException) { showHelp = true; } if (showHelp || (args.Length == 0)) { ShowHelp(options); return; } if (!string.IsNullOrEmpty(queryNETAssembly)) { const string FRAMEWORK_PREFIX = "FrameworkVersion="; bool bFoundVersion = false; string assemblyFile = (queryNETAssembly.EndsWith(".dll")) ? queryNETAssembly : Path.Combine(queryNETAssembly, Constants.UNITY_ASSEMBLY_LIB); //AssemblyName assemblyName = Util.FindAssemblyRef (assemblyFile, "mscorlib"); AssemblyName assemblyName = null; if (assemblyName != null) { // Found a reference, but surprisingly the local mscorlib assembly itself might have // a higher version; we need to check the local file. string localLib = Path.Combine(Path.GetDirectoryName(assemblyFile), "mscorlib.dll"); string version = (File.Exists(localLib)) ? System.Diagnostics.FileVersionInfo.GetVersionInfo(localLib).FileVersion : assemblyName.Version.ToString(); Console.WriteLine($"{FRAMEWORK_PREFIX}{version}"); bFoundVersion = true; } // We couldn't find a NET reference - lets see if there's a mscorlib assembly // next to the game assembly. string potentialMscorlibFilePath = Path.Combine(Path.GetDirectoryName(assemblyFile), "mscorlib.dll"); if (!bFoundVersion && File.Exists(potentialMscorlibFilePath)) { string version = System.Diagnostics.FileVersionInfo.GetVersionInfo(potentialMscorlibFilePath).FileVersion; Console.WriteLine($"{FRAMEWORK_PREFIX}{version}"); bFoundVersion = true; } if (!bFoundVersion) { // We will have to rely on the assembly's runtime version. Assembly gameAss = Assembly.ReflectionOnlyLoadFrom(assemblyFile); Console.WriteLine($"{FRAMEWORK_PREFIX}{gameAss.ImageRuntimeVersion}"); } // This is a query operation, as mentioned above // we're not going to patch the game assembly. return; } if (!string.IsNullOrEmpty(queryAssemblyName)) { string [] parsed = queryAssemblyName.Split(new string [] { "::" }, StringSplitOptions.None); if (parsed.Length != 2) { string strError = JSONResponse.CreateSerializedResponse("Invalid value, please respect format: 'Assembly_Path::Ref_Assembly_Name'", 1); Console.Error.WriteLine(strError); return; } string assemblyFile = (parsed [0].EndsWith(".dll")) ? parsed [0] : Path.Combine(parsed [0], Constants.UNITY_ASSEMBLY_LIB); //AssemblyName assemblyName = Util.FindAssemblyRef (assemblyFile, parsed [1]); AssemblyName assemblyName = null; if (assemblyName != null) { Console.WriteLine($"FoundAssembly={assemblyName.FullName}"); } // This is a query operation, as mentioned above // we're not going to patch the game assembly. return; } Run(removePatch); }