CodePatcher.cs 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. #if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Runtime.CompilerServices;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using SingularityGroup.HotReload.DTO;
  12. using JetBrains.Annotations;
  13. using SingularityGroup.HotReload.Burst;
  14. using SingularityGroup.HotReload.HarmonyLib;
  15. using SingularityGroup.HotReload.JsonConverters;
  16. using SingularityGroup.HotReload.MonoMod.Utils;
  17. using SingularityGroup.HotReload.Newtonsoft.Json;
  18. using SingularityGroup.HotReload.RuntimeDependencies;
  19. #if UNITY_EDITOR
  20. using UnityEditor;
  21. using UnityEditorInternal;
  22. #endif
  23. using UnityEngine;
  24. using UnityEngine.SceneManagement;
  25. [assembly: InternalsVisibleTo("SingularityGroup.HotReload.Editor")]
  26. namespace SingularityGroup.HotReload {
  27. class RegisterPatchesResult {
  28. // note: doesn't include removals and method definition changes (e.g. renames)
  29. public readonly List<MethodPatch> patchedMethods = new List<MethodPatch>();
  30. public List<SField> addedFields = new List<SField>();
  31. public readonly List<SMethod> patchedSMethods = new List<SMethod>();
  32. public bool inspectorModified;
  33. public readonly List<Tuple<SMethod, string>> patchFailures = new List<Tuple<SMethod, string>>();
  34. public readonly List<string> patchExceptions = new List<string>();
  35. }
  36. class FieldHandler {
  37. public readonly Action<Type, FieldInfo> storeField;
  38. public readonly Action<Type, FieldInfo, FieldInfo> registerInspectorFieldAttributes;
  39. public readonly Func<Type, string, bool> hideField;
  40. public FieldHandler(Action<Type, FieldInfo> storeField, Func<Type, string, bool> hideField, Action<Type, FieldInfo, FieldInfo> registerInspectorFieldAttributes) {
  41. this.storeField = storeField;
  42. this.hideField = hideField;
  43. this.registerInspectorFieldAttributes = registerInspectorFieldAttributes;
  44. }
  45. }
  46. class CodePatcher {
  47. public static readonly CodePatcher I = new CodePatcher();
  48. /// <summary>Tag for use in Debug.Log.</summary>
  49. public const string TAG = "HotReload";
  50. internal int PatchesApplied { get; private set; }
  51. string PersistencePath {get;}
  52. List<MethodPatchResponse> pendingPatches;
  53. readonly List<MethodPatchResponse> patchHistory;
  54. readonly HashSet<string> seenResponses = new HashSet<string>();
  55. string[] assemblySearchPaths;
  56. SymbolResolver symbolResolver;
  57. readonly string tmpDir;
  58. public FieldHandler fieldHandler;
  59. public bool debuggerCompatibilityEnabled;
  60. CodePatcher() {
  61. pendingPatches = new List<MethodPatchResponse>();
  62. patchHistory = new List<MethodPatchResponse>();
  63. if(UnityHelper.IsEditor) {
  64. tmpDir = PackageConst.LibraryCachePath;
  65. } else {
  66. tmpDir = UnityHelper.TemporaryCachePath;
  67. }
  68. if(!UnityHelper.IsEditor) {
  69. PersistencePath = Path.Combine(UnityHelper.PersistentDataPath, "HotReload", "patches.json");
  70. try {
  71. LoadPatches(PersistencePath);
  72. } catch(Exception ex) {
  73. Log.Error("Encountered exception when loading patches from disk:\n{0}", ex);
  74. }
  75. }
  76. }
  77. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
  78. static void InitializeUnityEvents() {
  79. UnityEventHelper.Initialize();
  80. }
  81. void LoadPatches(string filePath) {
  82. PlayerLog("Loading patches from file {0}", filePath);
  83. var file = new FileInfo(filePath);
  84. if(file.Exists) {
  85. var bytes = File.ReadAllText(filePath);
  86. var patches = JsonConvert.DeserializeObject<List<MethodPatchResponse>>(bytes);
  87. PlayerLog("Loaded {0} patches from disk", patches.Count.ToString());
  88. foreach (var patch in patches) {
  89. RegisterPatches(patch, persist: false);
  90. }
  91. }
  92. }
  93. internal IReadOnlyList<MethodPatchResponse> PendingPatches => pendingPatches;
  94. internal SymbolResolver SymbolResolver => symbolResolver;
  95. internal string[] GetAssemblySearchPaths() {
  96. EnsureSymbolResolver();
  97. return assemblySearchPaths;
  98. }
  99. internal RegisterPatchesResult RegisterPatches(MethodPatchResponse patches, bool persist) {
  100. PlayerLog("Register patches.\nWarnings: {0} \nMethods:\n{1}", string.Join("\n", patches.failures), string.Join("\n", patches.patches.SelectMany(p => p.modifiedMethods).Select(m => m.displayName)));
  101. pendingPatches.Add(patches);
  102. return ApplyPatches(persist);
  103. }
  104. RegisterPatchesResult ApplyPatches(bool persist) {
  105. PlayerLog("ApplyPatches. {0} patches pending.", pendingPatches.Count);
  106. EnsureSymbolResolver();
  107. var result = new RegisterPatchesResult();
  108. try {
  109. int count = 0;
  110. foreach(var response in pendingPatches) {
  111. if (seenResponses.Contains(response.id)) {
  112. continue;
  113. }
  114. foreach (var patch in response.patches) {
  115. var asm = Assembly.Load(patch.patchAssembly, patch.patchPdb);
  116. SymbolResolver.AddAssembly(asm);
  117. }
  118. HandleRemovedUnityMethods(response.removedMethod);
  119. #if UNITY_EDITOR
  120. HandleAlteredFields(response.id, result, response.alteredFields);
  121. #endif
  122. // needs to come before RegisterNewFieldInitializers
  123. RegisterNewFieldDefinitions(response);
  124. // Note: order is important here. Reshaped fields require new field initializers to be added
  125. // because the old initializers must override new initilaizers for existing holders.
  126. // so that the initializer is not invoked twice
  127. RegisterNewFieldInitializers(response);
  128. HandleReshapedFields(response);
  129. RemoveOldFieldInitializers(response);
  130. #if UNITY_EDITOR
  131. RegisterInspectorFieldAttributes(result, response);
  132. #endif
  133. HandleMethodPatchResponse(response, result);
  134. patchHistory.Add(response);
  135. seenResponses.Add(response.id);
  136. count += response.patches.Length;
  137. }
  138. if (count > 0) {
  139. Dispatch.OnHotReload(result.patchedMethods).Forget();
  140. }
  141. } catch(Exception ex) {
  142. Log.Warning("Exception occured when handling method patch. Exception:\n{0}", ex);
  143. } finally {
  144. pendingPatches.Clear();
  145. }
  146. if(PersistencePath != null && persist) {
  147. SaveAppliedPatches(PersistencePath).Forget();
  148. }
  149. PatchesApplied++;
  150. return result;
  151. }
  152. internal void ClearPatchedMethods() {
  153. PatchesApplied = 0;
  154. }
  155. static bool didLog;
  156. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
  157. static void WarnOnSceneLoad() {
  158. SceneManager.sceneLoaded += (_, __) => {
  159. if (didLog || !UnityEventHelper.UnityMethodsAdded()) {
  160. return;
  161. }
  162. Log.Warning("A new Scene was loaded while new unity event methods were added at runtime. MonoBehaviours in the Scene will not trigger these new events.");
  163. didLog = true;
  164. };
  165. }
  166. void HandleMethodPatchResponse(MethodPatchResponse response, RegisterPatchesResult result) {
  167. EnsureSymbolResolver();
  168. foreach(var patch in response.patches) {
  169. try {
  170. foreach(var sMethod in patch.newMethods) {
  171. var newMethod = SymbolResolver.Resolve(sMethod);
  172. try {
  173. UnityEventHelper.EnsureUnityEventMethod(newMethod);
  174. } catch(Exception ex) {
  175. Log.Warning("Encountered exception in EnsureUnityEventMethod: {0} {1}", ex.GetType().Name, ex.Message);
  176. }
  177. MethodUtils.DisableVisibilityChecks(newMethod);
  178. if (!patch.patchMethods.Any(m => m.metadataToken == sMethod.metadataToken)) {
  179. result.patchedMethods.Add(new MethodPatch(null, null, newMethod));
  180. result.patchedSMethods.Add(sMethod);
  181. previousPatchMethods[newMethod] = newMethod;
  182. newMethods.Add(newMethod);
  183. }
  184. }
  185. for (int i = 0; i < patch.modifiedMethods.Length; i++) {
  186. var sOriginalMethod = patch.modifiedMethods[i];
  187. var sPatchMethod = patch.patchMethods[i];
  188. var err = PatchMethod(response.id, sOriginalMethod: sOriginalMethod, sPatchMethod: sPatchMethod, containsBurstJobs: patch.unityJobs.Length > 0, patchesResult: result);
  189. if (!string.IsNullOrEmpty(err)) {
  190. result.patchFailures.Add(Tuple.Create(sOriginalMethod, err));
  191. }
  192. }
  193. foreach (var job in patch.unityJobs) {
  194. var type = SymbolResolver.Resolve(new SType(patch.assemblyName, job.jobKind.ToString(), job.metadataToken));
  195. JobHotReloadUtility.HotReloadBurstCompiledJobs(job, type);
  196. }
  197. #if UNITY_EDITOR
  198. HandleNewFields(patch.patchId, result, patch.newFields);
  199. #endif
  200. } catch (Exception ex) {
  201. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.Exception), new EditorExtraData {
  202. { StatKey.PatchId, patch.patchId },
  203. { StatKey.Detailed_Exception, ex.ToString() },
  204. }).Forget();
  205. result.patchExceptions.Add($"Edit requires full recompile to apply: Encountered exception when applying a patch.\nCommon causes: editing code that failed to patch previously, an unsupported change, or a real bug in Hot Reload.\nIf you think this is a bug, please report the issue on Discord and include a code-snippet before/after.\nException: {ex}");
  206. }
  207. }
  208. }
  209. void HandleRemovedUnityMethods(SMethod[] removedMethods) {
  210. if (removedMethods == null) {
  211. return;
  212. }
  213. foreach(var sMethod in removedMethods) {
  214. try {
  215. var oldMethod = SymbolResolver.Resolve(sMethod);
  216. UnityEventHelper.RemoveUnityEventMethod(oldMethod);
  217. } catch (SymbolResolvingFailedException) {
  218. // ignore, not a unity event method if can't resolve
  219. } catch(Exception ex) {
  220. Log.Warning("Encountered exception in RemoveUnityEventMethod: {0} {1}", ex.GetType().Name, ex.Message);
  221. }
  222. }
  223. }
  224. // Important: must come before applying any patches
  225. void RegisterNewFieldInitializers(MethodPatchResponse resp) {
  226. for (var i = 0; i < resp.addedFieldInitializerFields.Length; i++) {
  227. var sField = resp.addedFieldInitializerFields[i];
  228. var sMethod = resp.addedFieldInitializerInitializers[i];
  229. try {
  230. var declaringType = SymbolResolver.Resolve(sField.declaringType);
  231. var method = SymbolResolver.Resolve(sMethod);
  232. if (!(method is MethodInfo initializer)) {
  233. Log.Warning($"Failed registering initializer for field {sField.fieldName} in {sField.declaringType.typeName}. Field value might not be initialized correctly. Invalid method.");
  234. continue;
  235. }
  236. // We infer if the field is static by the number of parameters the method has
  237. // because sField is old field
  238. var isStatic = initializer.GetParameters().Length == 0;
  239. MethodUtils.DisableVisibilityChecks(initializer);
  240. // Initializer return type is used in place of fieldType because latter might be point to old field if the type changed
  241. FieldInitializerRegister.RegisterInitializer(declaringType, sField.fieldName, initializer.ReturnType, initializer, isStatic);
  242. } catch (Exception e) {
  243. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.RegisterFieldInitializer), new EditorExtraData {
  244. { StatKey.PatchId, resp.id },
  245. { StatKey.Detailed_Exception, e.ToString() },
  246. }).Forget();
  247. Log.Warning($"Failed registering initializer for field {sField.fieldName} in {sField.declaringType.typeName}. Field value might not be initialized correctly. Exception: {e.Message}");
  248. }
  249. }
  250. }
  251. void RegisterNewFieldDefinitions(MethodPatchResponse resp) {
  252. foreach (var sField in resp.newFieldDefinitions) {
  253. try {
  254. var declaringType = SymbolResolver.Resolve(sField.declaringType);
  255. var fieldType = SymbolResolver.Resolve(sField).FieldType;
  256. FieldResolver.RegisterFieldType(declaringType, sField.fieldName, fieldType);
  257. } catch (Exception e) {
  258. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.RegisterFieldDefinition), new EditorExtraData {
  259. { StatKey.PatchId, resp.id },
  260. { StatKey.Detailed_Exception, e.ToString() },
  261. }).Forget();
  262. Log.Warning($"Failed registering new field definitions for field {sField.fieldName} in {sField.declaringType.typeName}. Exception: {e.Message}");
  263. }
  264. }
  265. }
  266. // Important: must come before applying any patches
  267. // Note: server might decide not to report removed field initializer at all if it can handle it
  268. void RemoveOldFieldInitializers(MethodPatchResponse resp) {
  269. foreach (var sField in resp.removedFieldInitializers) {
  270. try {
  271. var declaringType = SymbolResolver.Resolve(sField.declaringType);
  272. var fieldType = SymbolResolver.Resolve(sField.declaringType);
  273. FieldInitializerRegister.UnregisterInitializer(declaringType, sField.fieldName, fieldType, sField.isStatic);
  274. } catch (Exception e) {
  275. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.UnregisterFieldInitializer), new EditorExtraData {
  276. { StatKey.PatchId, resp.id },
  277. { StatKey.Detailed_Exception, e.ToString() },
  278. }).Forget();
  279. Log.Warning($"Failed removing initializer for field {sField.fieldName} in {sField.declaringType.typeName}. Field value might not be initialized correctly. Exception: {e.Message}");
  280. }
  281. }
  282. }
  283. // Important: must come before applying any patches
  284. // Should also come after RegisterNewFieldInitializers so that new initializers are not invoked for existing objects
  285. internal void HandleReshapedFields(MethodPatchResponse resp) {
  286. foreach(var patch in resp.patches) {
  287. var removedReshapedFields = patch.deletedFields;
  288. var renamedReshapedFieldsFrom = patch.renamedFieldsFrom;
  289. var renamedReshapedFieldsTo = patch.renamedFieldsTo;
  290. foreach (var f in removedReshapedFields) {
  291. try {
  292. var declaringType = SymbolResolver.Resolve(f.declaringType);
  293. var fieldType = SymbolResolver.Resolve(f).FieldType;
  294. FieldResolver.ClearHolders(declaringType, f.isStatic, f.fieldName, fieldType);
  295. } catch (Exception e) {
  296. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.ClearHolders), new EditorExtraData {
  297. { StatKey.PatchId, resp.id },
  298. { StatKey.Detailed_Exception, e.ToString() },
  299. }).Forget();
  300. Log.Warning($"Failed removing field value from {f.fieldName} in {f.declaringType.typeName}. Field value in code might not be up to date. Exception: {e.Message}");
  301. }
  302. }
  303. for (var i = 0; i < renamedReshapedFieldsFrom.Length; i++) {
  304. var fromField = renamedReshapedFieldsFrom[i];
  305. var toField = renamedReshapedFieldsTo[i];
  306. try {
  307. var declaringType = SymbolResolver.Resolve(fromField.declaringType);
  308. var fieldType = SymbolResolver.Resolve(fromField).FieldType;
  309. var toFieldType = SymbolResolver.Resolve(toField).FieldType;
  310. if (!AreSTypesCompatible(fromField.declaringType, toField.declaringType)
  311. || fieldType != toFieldType
  312. || fromField.isStatic != toField.isStatic
  313. ) {
  314. FieldResolver.ClearHolders(declaringType, fromField.isStatic, fromField.fieldName, fieldType);
  315. continue;
  316. }
  317. FieldResolver.MoveHolders(declaringType, fromField.fieldName, toField.fieldName, fieldType, fromField.isStatic);
  318. } catch (Exception e) {
  319. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.MoveHolders), new EditorExtraData {
  320. { StatKey.PatchId, resp.id },
  321. { StatKey.Detailed_Exception, e.ToString() },
  322. }).Forget();
  323. Log.Warning($"Failed moving field value from {fromField} to {toField} in {toField.declaringType.typeName}. Field value in code might not be up to date. Exception: {e.Message}");
  324. }
  325. }
  326. }
  327. }
  328. internal bool AreSTypesCompatible(SType one, SType two) {
  329. if (one.isGenericParameter != two.isGenericParameter) {
  330. return false;
  331. }
  332. if (one.metadataToken != two.metadataToken) {
  333. return false;
  334. }
  335. if (one.assemblyName != two.assemblyName) {
  336. return false;
  337. }
  338. if (one.genericParameterPosition != two.genericParameterPosition) {
  339. return false;
  340. }
  341. if (one.typeName != two.typeName) {
  342. return false;
  343. }
  344. return true;
  345. }
  346. #if UNITY_EDITOR
  347. internal void RegisterInspectorFieldAttributes(RegisterPatchesResult result, MethodPatchResponse resp) {
  348. foreach (var patch in resp.patches) {
  349. var propertyAttributesFieldOriginal = patch.propertyAttributesFieldOriginal ?? Array.Empty<SField>();
  350. var propertyAttributesFieldUpdated = patch.propertyAttributesFieldUpdated ?? Array.Empty<SField>();
  351. for (var i = 0; i < propertyAttributesFieldOriginal.Length; i++) {
  352. var original = propertyAttributesFieldOriginal[i];
  353. var updated = propertyAttributesFieldUpdated[i];
  354. try {
  355. var declaringType = SymbolResolver.Resolve(original.declaringType);
  356. var originalField = SymbolResolver.Resolve(original);
  357. var updatedField = SymbolResolver.Resolve(updated);
  358. fieldHandler?.registerInspectorFieldAttributes?.Invoke(declaringType, originalField, updatedField);
  359. result.inspectorModified = true;
  360. } catch (Exception e) {
  361. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.MoveHolders), new EditorExtraData {
  362. { StatKey.PatchId, resp.id },
  363. { StatKey.Detailed_Exception, e.ToString() },
  364. }).Forget();
  365. Log.Warning($"Failed updating field attributes of {original.fieldName} in {original.declaringType.typeName}. Updates might not reflect in the inspector. Exception: {e.Message}");
  366. }
  367. }
  368. }
  369. }
  370. internal void HandleNewFields(string patchId, RegisterPatchesResult result, SField[] sFields) {
  371. foreach (var sField in sFields) {
  372. if (!sField.serializable) {
  373. continue;
  374. }
  375. try {
  376. var declaringType = SymbolResolver.Resolve(sField.declaringType);
  377. var field = SymbolResolver.Resolve(sField);
  378. fieldHandler?.storeField?.Invoke(declaringType, field);
  379. result.inspectorModified = true;
  380. } catch (Exception e) {
  381. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.AddInspectorField), new EditorExtraData {
  382. { StatKey.PatchId, patchId },
  383. { StatKey.Detailed_Exception, e.ToString() },
  384. }).Forget();
  385. Log.Warning($"Failed adding field {sField.fieldName}:{sField.declaringType.typeName} to the inspector. Field will not be displayed. Exception: {e.Message}");
  386. }
  387. }
  388. result.addedFields.AddRange(sFields);
  389. }
  390. // IMPORTANT: must come before HandleNewFields. Might contain new fields which we don't want to hide
  391. internal void HandleAlteredFields(string patchId, RegisterPatchesResult result, SField[] alteredFields) {
  392. if (alteredFields == null) {
  393. return;
  394. }
  395. bool alteredFieldHidden = false;
  396. foreach(var sField in alteredFields) {
  397. try {
  398. var declaringType = SymbolResolver.Resolve(sField.declaringType);
  399. if (fieldHandler?.hideField?.Invoke(declaringType, sField.fieldName) == true) {
  400. alteredFieldHidden = true;
  401. }
  402. } catch(Exception e) {
  403. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.HideInspectorField), new EditorExtraData {
  404. { StatKey.PatchId, patchId },
  405. { StatKey.Detailed_Exception, e.ToString() },
  406. }).Forget();
  407. Log.Warning($"Failed hiding field {sField.fieldName}:{sField.declaringType.typeName} from the inspector. Exception: {e.Message}");
  408. }
  409. }
  410. if (alteredFieldHidden) {
  411. result.inspectorModified = true;
  412. }
  413. }
  414. #endif
  415. Dictionary<MethodBase, MethodBase> previousPatchMethods = new Dictionary<MethodBase, MethodBase>();
  416. public IEnumerable<MethodBase> OriginalPatchMethods => previousPatchMethods.Keys;
  417. List<MethodBase> newMethods = new List<MethodBase>();
  418. string PatchMethod(string patchId, SMethod sOriginalMethod, SMethod sPatchMethod, bool containsBurstJobs, RegisterPatchesResult patchesResult) {
  419. try {
  420. var patchMethod = SymbolResolver.Resolve(sPatchMethod);
  421. var start = DateTime.UtcNow;
  422. var state = TryResolveMethod(sOriginalMethod, patchMethod);
  423. if (Debugger.IsAttached && !debuggerCompatibilityEnabled) {
  424. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.DebuggerAttached), new EditorExtraData {
  425. { StatKey.PatchId, patchId },
  426. }).Forget();
  427. return "Patching methods is not allowed while the Debugger is attached. You can change this behavior in settings if Hot Reload is compatible with the debugger you're running.";
  428. }
  429. if (DateTime.UtcNow - start > TimeSpan.FromMilliseconds(500)) {
  430. Log.Info("Hot Reload apply took {0}", (DateTime.UtcNow - start).TotalMilliseconds);
  431. }
  432. if(state.match == null) {
  433. var error = "Edit requires full recompile to apply: Method mismatch: {0}, patch: {1}. \nCommon causes: editing code that failed to patch previously, an unsupported change, or a real bug in Hot Reload.\nIf you think this is a bug, please report the issue on Discord and include a code-snippet before/after.";
  434. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.MethodMismatch), new EditorExtraData {
  435. { StatKey.PatchId, patchId },
  436. }).Forget();
  437. return string.Format(error, sOriginalMethod.simpleName, patchMethod.Name);
  438. }
  439. PlayerLog("Detour method {0:X8} {1}, offset: {2}", sOriginalMethod.metadataToken, patchMethod.Name, state.offset);
  440. DetourResult result;
  441. DetourApi.DetourMethod(state.match, patchMethod, out result);
  442. if (result.success) {
  443. // previous method is either original method or the last patch method
  444. MethodBase previousMethod;
  445. if (!previousPatchMethods.TryGetValue(state.match, out previousMethod)) {
  446. previousMethod = state.match;
  447. }
  448. MethodBase originalMethod = state.match;
  449. if (newMethods.Contains(state.match)) {
  450. // for function added at runtime the original method should be null
  451. originalMethod = null;
  452. }
  453. patchesResult.patchedMethods.Add(new MethodPatch(originalMethod, previousMethod, patchMethod));
  454. patchesResult.patchedSMethods.Add(sOriginalMethod);
  455. previousPatchMethods[state.match] = patchMethod;
  456. try {
  457. Dispatch.OnHotReloadLocal(state.match, patchMethod);
  458. } catch {
  459. // best effort
  460. }
  461. return null;
  462. } else {
  463. if(result.exception is InvalidProgramException && containsBurstJobs) {
  464. //ignore. The method is likely burst compiled and can't be patched
  465. return null;
  466. } else {
  467. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.Failure), new EditorExtraData {
  468. { StatKey.PatchId, patchId },
  469. { StatKey.Detailed_Exception, result.exception.ToString() },
  470. }).Forget();
  471. return HandleMethodPatchFailure(sOriginalMethod, result.exception);
  472. }
  473. }
  474. } catch(Exception ex) {
  475. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.Exception), new EditorExtraData {
  476. { StatKey.PatchId, patchId },
  477. { StatKey.Detailed_Exception, ex.ToString() },
  478. }).Forget();
  479. return HandleMethodPatchFailure(sOriginalMethod, ex);
  480. }
  481. }
  482. struct ResolveMethodState {
  483. public readonly SMethod originalMethod;
  484. public readonly int offset;
  485. public readonly bool tryLowerTokens;
  486. public readonly bool tryHigherTokens;
  487. public readonly MethodBase match;
  488. public ResolveMethodState(SMethod originalMethod, int offset, bool tryLowerTokens, bool tryHigherTokens, MethodBase match) {
  489. this.originalMethod = originalMethod;
  490. this.offset = offset;
  491. this.tryLowerTokens = tryLowerTokens;
  492. this.tryHigherTokens = tryHigherTokens;
  493. this.match = match;
  494. }
  495. public ResolveMethodState With(bool? tryLowerTokens = null, bool? tryHigherTokens = null, MethodBase match = null, int? offset = null) {
  496. return new ResolveMethodState(
  497. originalMethod,
  498. offset ?? this.offset,
  499. tryLowerTokens ?? this.tryLowerTokens,
  500. tryHigherTokens ?? this.tryHigherTokens,
  501. match ?? this.match);
  502. }
  503. }
  504. struct ResolveMethodResult {
  505. public readonly MethodBase resolvedMethod;
  506. public readonly bool tokenOutOfRange;
  507. public ResolveMethodResult(MethodBase resolvedMethod, bool tokenOutOfRange) {
  508. this.resolvedMethod = resolvedMethod;
  509. this.tokenOutOfRange = tokenOutOfRange;
  510. }
  511. }
  512. ResolveMethodState TryResolveMethod(SMethod originalMethod, MethodBase patchMethod) {
  513. var state = new ResolveMethodState(originalMethod, offset: 0, tryLowerTokens: true, tryHigherTokens: true, match: null);
  514. var result = TryResolveMethodCore(state.originalMethod, patchMethod, 0);
  515. if(result.resolvedMethod != null) {
  516. return state.With(match: result.resolvedMethod);
  517. }
  518. state = state.With(offset: 1);
  519. const int tries = 100000;
  520. while(state.offset <= tries && (state.tryHigherTokens || state.tryLowerTokens)) {
  521. if(state.tryHigherTokens) {
  522. result = TryResolveMethodCore(originalMethod, patchMethod, state.offset);
  523. if(result.resolvedMethod != null) {
  524. return state.With(match: result.resolvedMethod);
  525. } else if(result.tokenOutOfRange) {
  526. state = state.With(tryHigherTokens: false);
  527. }
  528. }
  529. if(state.tryLowerTokens) {
  530. result = TryResolveMethodCore(originalMethod, patchMethod, -state.offset);
  531. if(result.resolvedMethod != null) {
  532. return state.With(match: result.resolvedMethod);
  533. } else if(result.tokenOutOfRange) {
  534. state = state.With(tryLowerTokens: false);
  535. }
  536. }
  537. state = state.With(offset: state.offset + 1);
  538. }
  539. return state;
  540. }
  541. ResolveMethodResult TryResolveMethodCore(SMethod methodToResolve, MethodBase patchMethod, int offset) {
  542. bool tokenOutOfRange = false;
  543. MethodBase resolvedMethod = null;
  544. try {
  545. resolvedMethod = TryGetMethodBaseWithRelativeToken(methodToResolve, offset);
  546. var err = MethodCompatiblity.CheckCompatibility(resolvedMethod, patchMethod);
  547. if(err != null) {
  548. // if (resolvedMethod.Name == patchMethod.Name) {
  549. // Log.Info(err);
  550. // }
  551. resolvedMethod = null;
  552. }
  553. } catch (SymbolResolvingFailedException ex) when(ex.InnerException is ArgumentOutOfRangeException) {
  554. tokenOutOfRange = true;
  555. } catch (ArgumentOutOfRangeException) {
  556. tokenOutOfRange = true;
  557. }
  558. return new ResolveMethodResult(resolvedMethod, tokenOutOfRange);
  559. }
  560. MethodBase TryGetMethodBaseWithRelativeToken(SMethod sOriginalMethod, int offset) {
  561. return symbolResolver.Resolve(new SMethod(sOriginalMethod.assemblyName,
  562. sOriginalMethod.displayName,
  563. sOriginalMethod.metadataToken + offset,
  564. sOriginalMethod.simpleName));
  565. }
  566. string HandleMethodPatchFailure(SMethod method, Exception exception) {
  567. return $"Edit requires full recompile to apply: Failed to apply patch for method {method.displayName} in assembly {method.assemblyName}.\nCommon causes: editing code that failed to patch previously, an unsupported change, or a real bug in Hot Reload.\nIf you think this is a bug, please report the issue on Discord and include a code-snippet before/after.\nException: {exception}";
  568. }
  569. void EnsureSymbolResolver() {
  570. if (symbolResolver == null) {
  571. var searchPaths = new HashSet<string>();
  572. var assemblies = AppDomain.CurrentDomain.GetAssemblies();
  573. var assembliesByName = new Dictionary<string, List<Assembly>>();
  574. for (var i = 0; i < assemblies.Length; i++) {
  575. var name = assemblies[i].GetNameSafe();
  576. List<Assembly> list;
  577. if (!assembliesByName.TryGetValue(name, out list)) {
  578. assembliesByName.Add(name, list = new List<Assembly>());
  579. }
  580. list.Add(assemblies[i]);
  581. if(assemblies[i].IsDynamic) continue;
  582. var location = assemblies[i].Location;
  583. if(File.Exists(location)) {
  584. searchPaths.Add(Path.GetDirectoryName(Path.GetFullPath(location)));
  585. }
  586. }
  587. symbolResolver = new SymbolResolver(assembliesByName);
  588. assemblySearchPaths = searchPaths.ToArray();
  589. }
  590. }
  591. //Allow one save operation at a time.
  592. readonly SemaphoreSlim gate = new SemaphoreSlim(1);
  593. public async Task SaveAppliedPatches(string filePath) {
  594. await gate.WaitAsync();
  595. try {
  596. await SaveAppliedPatchesNoLock(filePath);
  597. } finally {
  598. gate.Release();
  599. }
  600. }
  601. async Task SaveAppliedPatchesNoLock(string filePath) {
  602. if (filePath == null) {
  603. throw new ArgumentNullException(nameof(filePath));
  604. }
  605. filePath = Path.GetFullPath(filePath);
  606. var dir = Path.GetDirectoryName(filePath);
  607. if(string.IsNullOrEmpty(dir)) {
  608. throw new ArgumentException("Invalid path: " + filePath, nameof(filePath));
  609. }
  610. Directory.CreateDirectory(dir);
  611. var history = patchHistory.ToList();
  612. PlayerLog("Saving {0} applied patches to {1}", history.Count, filePath);
  613. await Task.Run(() => {
  614. using (FileStream fs = File.Create(filePath))
  615. using (StreamWriter sw = new StreamWriter(fs))
  616. using (JsonWriter writer = new JsonTextWriter(sw)) {
  617. JsonSerializer serializer = JsonSerializer.Create(new JsonSerializerSettings {
  618. Converters = new List<JsonConverter> { new MethodPatchResponsesConverter() }
  619. });
  620. serializer.Serialize(writer, history);
  621. }
  622. });
  623. }
  624. public void InitPatchesBlocked(string filePath) {
  625. seenResponses.Clear();
  626. var file = new FileInfo(filePath);
  627. if (file.Exists) {
  628. using(var fs = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan))
  629. using (StreamReader sr = new StreamReader(fs))
  630. using (JsonReader reader = new JsonTextReader(sr)) {
  631. JsonSerializer serializer = JsonSerializer.Create(new JsonSerializerSettings {
  632. Converters = new List<JsonConverter> { new MethodPatchResponsesConverter() }
  633. });
  634. pendingPatches = serializer.Deserialize<List<MethodPatchResponse>>(reader);
  635. }
  636. ApplyPatches(persist: false);
  637. }
  638. }
  639. [StringFormatMethod("format")]
  640. static void PlayerLog(string format, params object[] args) {
  641. #if !UNITY_EDITOR
  642. HotReload.Log.Info(format, args);
  643. #endif //!UNITY_EDITOR
  644. }
  645. class SimpleMethodComparer : IEqualityComparer<SMethod> {
  646. public static readonly SimpleMethodComparer I = new SimpleMethodComparer();
  647. SimpleMethodComparer() { }
  648. public bool Equals(SMethod x, SMethod y) => x.metadataToken == y.metadataToken;
  649. public int GetHashCode(SMethod x) {
  650. return x.metadataToken;
  651. }
  652. }
  653. }
  654. }
  655. #endif