updated: hot reload to 1.13.7

This commit is contained in:
Chris
2025-07-09 23:24:14 -04:00
parent 236f9ea5df
commit 7641b83b6a
45 changed files with 2018 additions and 462 deletions

View File

@@ -1,11 +1,12 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Reflection;
using SingularityGroup.HotReload.DTO;
namespace SingularityGroup.HotReload.Burst {
public static class JobHotReloadUtility {
public static void HotReloadBurstCompiledJobs(CodePatch patch, Module module) {
JobPatchUtility.PatchBurstCompiledJobs(patch, module, unityMajorVersion:
public static void HotReloadBurstCompiledJobs(SUnityJob jobData, Type proxyJobType) {
JobPatchUtility.PatchBurstCompiledJobs(jobData, proxyJobType, unityMajorVersion:
#if UNITY_2022_2_OR_NEWER
2022
#elif UNITY_2021_3_OR_NEWER

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -13,8 +14,13 @@ using JetBrains.Annotations;
using SingularityGroup.HotReload.Burst;
using SingularityGroup.HotReload.HarmonyLib;
using SingularityGroup.HotReload.JsonConverters;
using SingularityGroup.HotReload.MonoMod.Utils;
using SingularityGroup.HotReload.Newtonsoft.Json;
using SingularityGroup.HotReload.RuntimeDependencies;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
#endif
using UnityEngine;
using UnityEngine.SceneManagement;
@@ -24,8 +30,23 @@ namespace SingularityGroup.HotReload {
class RegisterPatchesResult {
// note: doesn't include removals and method definition changes (e.g. renames)
public readonly List<MethodPatch> patchedMethods = new List<MethodPatch>();
public List<SField> addedFields = new List<SField>();
public readonly List<SMethod> patchedSMethods = new List<SMethod>();
public bool inspectorModified;
public readonly List<Tuple<SMethod, string>> patchFailures = new List<Tuple<SMethod, string>>();
public readonly List<string> patchExceptions = new List<string>();
}
class FieldHandler {
public readonly Action<Type, FieldInfo> storeField;
public readonly Action<Type, FieldInfo, FieldInfo> registerInspectorFieldAttributes;
public readonly Func<Type, string, bool> hideField;
public FieldHandler(Action<Type, FieldInfo> storeField, Func<Type, string, bool> hideField, Action<Type, FieldInfo, FieldInfo> registerInspectorFieldAttributes) {
this.storeField = storeField;
this.hideField = hideField;
this.registerInspectorFieldAttributes = registerInspectorFieldAttributes;
}
}
class CodePatcher {
@@ -42,6 +63,8 @@ namespace SingularityGroup.HotReload {
string[] assemblySearchPaths;
SymbolResolver symbolResolver;
readonly string tmpDir;
public FieldHandler fieldHandler;
public bool debuggerCompatibilityEnabled;
CodePatcher() {
pendingPatches = new List<MethodPatchResponse>();
@@ -108,6 +131,26 @@ namespace SingularityGroup.HotReload {
if (seenResponses.Contains(response.id)) {
continue;
}
foreach (var patch in response.patches) {
var asm = Assembly.Load(patch.patchAssembly, patch.patchPdb);
SymbolResolver.AddAssembly(asm);
}
HandleRemovedUnityMethods(response.removedMethod);
#if UNITY_EDITOR
HandleAlteredFields(response.id, result, response.alteredFields);
#endif
// needs to come before RegisterNewFieldInitializers
RegisterNewFieldDefinitions(response);
// Note: order is important here. Reshaped fields require new field initializers to be added
// because the old initializers must override new initilaizers for existing holders.
// so that the initializer is not invoked twice
RegisterNewFieldInitializers(response);
HandleReshapedFields(response);
RemoveOldFieldInitializers(response);
#if UNITY_EDITOR
RegisterInspectorFieldAttributes(result, response);
#endif
HandleMethodPatchResponse(response, result);
patchHistory.Add(response);
@@ -152,11 +195,8 @@ namespace SingularityGroup.HotReload {
foreach(var patch in response.patches) {
try {
var asm = Assembly.Load(patch.patchAssembly, patch.patchPdb);
var module = asm.GetLoadedModules()[0];
foreach(var sMethod in patch.newMethods) {
var newMethod = module.ResolveMethod(sMethod.metadataToken);
var newMethod = SymbolResolver.Resolve(sMethod);
try {
UnityEventHelper.EnsureUnityEventMethod(newMethod);
} catch(Exception ex) {
@@ -171,43 +211,273 @@ namespace SingularityGroup.HotReload {
}
}
symbolResolver.AddAssembly(asm);
for (int i = 0; i < patch.modifiedMethods.Length; i++) {
var sOriginalMethod = patch.modifiedMethods[i];
var sPatchMethod = patch.patchMethods[i];
var err = PatchMethod(module: module, sOriginalMethod: sOriginalMethod, sPatchMethod: sPatchMethod, containsBurstJobs: patch.unityJobs.Length > 0, patchesResult: result);
var err = PatchMethod(response.id, sOriginalMethod: sOriginalMethod, sPatchMethod: sPatchMethod, containsBurstJobs: patch.unityJobs.Length > 0, patchesResult: result);
if (!string.IsNullOrEmpty(err)) {
result.patchFailures.Add(Tuple.Create(sOriginalMethod, err));
}
}
JobHotReloadUtility.HotReloadBurstCompiledJobs(patch, module);
foreach (var job in patch.unityJobs) {
var type = SymbolResolver.Resolve(new SType(patch.assemblyName, job.jobKind.ToString(), job.metadataToken));
JobHotReloadUtility.HotReloadBurstCompiledJobs(job, type);
}
#if UNITY_EDITOR
HandleNewFields(patch.patchId, result, patch.newFields);
#endif
} catch (Exception ex) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.Exception), new EditorExtraData {
{ StatKey.PatchId, patch.patchId },
{ StatKey.Detailed_Exception, ex.ToString() },
}).Forget();
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}");
}
}
}
void HandleRemovedUnityMethods(SMethod[] removedMethods) {
if (removedMethods == null) {
return;
}
foreach(var sMethod in removedMethods) {
try {
var oldMethod = SymbolResolver.Resolve(sMethod);
UnityEventHelper.RemoveUnityEventMethod(oldMethod);
} catch (SymbolResolvingFailedException) {
// ignore, not a unity event method if can't resolve
} catch(Exception ex) {
Log.Warning("Failed to apply patch with id: {0}\n{1}", patch.patchId, ex);
Log.Warning("Encountered exception in RemoveUnityEventMethod: {0} {1}", ex.GetType().Name, ex.Message);
}
}
}
// Important: must come before applying any patches
void RegisterNewFieldInitializers(MethodPatchResponse resp) {
for (var i = 0; i < resp.addedFieldInitializerFields.Length; i++) {
var sField = resp.addedFieldInitializerFields[i];
var sMethod = resp.addedFieldInitializerInitializers[i];
try {
var declaringType = SymbolResolver.Resolve(sField.declaringType);
var method = SymbolResolver.Resolve(sMethod);
if (!(method is MethodInfo initializer)) {
Log.Warning($"Failed registering initializer for field {sField.fieldName} in {sField.declaringType.typeName}. Field value might not be initialized correctly. Invalid method.");
continue;
}
// We infer if the field is static by the number of parameters the method has
// because sField is old field
var isStatic = initializer.GetParameters().Length == 0;
MethodUtils.DisableVisibilityChecks(initializer);
// Initializer return type is used in place of fieldType because latter might be point to old field if the type changed
FieldInitializerRegister.RegisterInitializer(declaringType, sField.fieldName, initializer.ReturnType, initializer, isStatic);
} catch (Exception e) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.RegisterFieldInitializer), new EditorExtraData {
{ StatKey.PatchId, resp.id },
{ StatKey.Detailed_Exception, e.ToString() },
}).Forget();
Log.Warning($"Failed registering initializer for field {sField.fieldName} in {sField.declaringType.typeName}. Field value might not be initialized correctly. Exception: {e.Message}");
}
}
}
void RegisterNewFieldDefinitions(MethodPatchResponse resp) {
foreach (var sField in resp.newFieldDefinitions) {
try {
var declaringType = SymbolResolver.Resolve(sField.declaringType);
var fieldType = SymbolResolver.Resolve(sField).FieldType;
FieldResolver.RegisterFieldType(declaringType, sField.fieldName, fieldType);
} catch (Exception e) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.RegisterFieldDefinition), new EditorExtraData {
{ StatKey.PatchId, resp.id },
{ StatKey.Detailed_Exception, e.ToString() },
}).Forget();
Log.Warning($"Failed registering new field definitions for field {sField.fieldName} in {sField.declaringType.typeName}. Exception: {e.Message}");
}
}
}
// Important: must come before applying any patches
// Note: server might decide not to report removed field initializer at all if it can handle it
void RemoveOldFieldInitializers(MethodPatchResponse resp) {
foreach (var sField in resp.removedFieldInitializers) {
try {
var declaringType = SymbolResolver.Resolve(sField.declaringType);
var fieldType = SymbolResolver.Resolve(sField.declaringType);
FieldInitializerRegister.UnregisterInitializer(declaringType, sField.fieldName, fieldType, sField.isStatic);
} catch (Exception e) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.UnregisterFieldInitializer), new EditorExtraData {
{ StatKey.PatchId, resp.id },
{ StatKey.Detailed_Exception, e.ToString() },
}).Forget();
Log.Warning($"Failed removing initializer for field {sField.fieldName} in {sField.declaringType.typeName}. Field value might not be initialized correctly. Exception: {e.Message}");
}
}
}
// Important: must come before applying any patches
// Should also come after RegisterNewFieldInitializers so that new initializers are not invoked for existing objects
internal void HandleReshapedFields(MethodPatchResponse resp) {
foreach(var patch in resp.patches) {
var removedReshapedFields = patch.deletedFields;
var renamedReshapedFieldsFrom = patch.renamedFieldsFrom;
var renamedReshapedFieldsTo = patch.renamedFieldsTo;
foreach (var f in removedReshapedFields) {
try {
var declaringType = SymbolResolver.Resolve(f.declaringType);
var fieldType = SymbolResolver.Resolve(f).FieldType;
FieldResolver.ClearHolders(declaringType, f.isStatic, f.fieldName, fieldType);
} catch (Exception e) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.ClearHolders), new EditorExtraData {
{ StatKey.PatchId, resp.id },
{ StatKey.Detailed_Exception, e.ToString() },
}).Forget();
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}");
}
}
for (var i = 0; i < renamedReshapedFieldsFrom.Length; i++) {
var fromField = renamedReshapedFieldsFrom[i];
var toField = renamedReshapedFieldsTo[i];
try {
var declaringType = SymbolResolver.Resolve(fromField.declaringType);
var fieldType = SymbolResolver.Resolve(fromField).FieldType;
var toFieldType = SymbolResolver.Resolve(toField).FieldType;
if (!AreSTypesCompatible(fromField.declaringType, toField.declaringType)
|| fieldType != toFieldType
|| fromField.isStatic != toField.isStatic
) {
FieldResolver.ClearHolders(declaringType, fromField.isStatic, fromField.fieldName, fieldType);
continue;
}
FieldResolver.MoveHolders(declaringType, fromField.fieldName, toField.fieldName, fieldType, fromField.isStatic);
} catch (Exception e) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.MoveHolders), new EditorExtraData {
{ StatKey.PatchId, resp.id },
{ StatKey.Detailed_Exception, e.ToString() },
}).Forget();
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}");
}
}
}
}
internal bool AreSTypesCompatible(SType one, SType two) {
if (one.isGenericParameter != two.isGenericParameter) {
return false;
}
if (one.metadataToken != two.metadataToken) {
return false;
}
if (one.assemblyName != two.assemblyName) {
return false;
}
if (one.genericParameterPosition != two.genericParameterPosition) {
return false;
}
if (one.typeName != two.typeName) {
return false;
}
return true;
}
#if UNITY_EDITOR
internal void RegisterInspectorFieldAttributes(RegisterPatchesResult result, MethodPatchResponse resp) {
foreach (var patch in resp.patches) {
var propertyAttributesFieldOriginal = patch.propertyAttributesFieldOriginal ?? Array.Empty<SField>();
var propertyAttributesFieldUpdated = patch.propertyAttributesFieldUpdated ?? Array.Empty<SField>();
for (var i = 0; i < propertyAttributesFieldOriginal.Length; i++) {
var original = propertyAttributesFieldOriginal[i];
var updated = propertyAttributesFieldUpdated[i];
try {
var declaringType = SymbolResolver.Resolve(original.declaringType);
var originalField = SymbolResolver.Resolve(original);
var updatedField = SymbolResolver.Resolve(updated);
fieldHandler?.registerInspectorFieldAttributes?.Invoke(declaringType, originalField, updatedField);
result.inspectorModified = true;
} catch (Exception e) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.MoveHolders), new EditorExtraData {
{ StatKey.PatchId, resp.id },
{ StatKey.Detailed_Exception, e.ToString() },
}).Forget();
Log.Warning($"Failed updating field attributes of {original.fieldName} in {original.declaringType.typeName}. Updates might not reflect in the inspector. Exception: {e.Message}");
}
}
}
}
internal void HandleNewFields(string patchId, RegisterPatchesResult result, SField[] sFields) {
foreach (var sField in sFields) {
if (!sField.serializable) {
continue;
}
try {
var declaringType = SymbolResolver.Resolve(sField.declaringType);
var field = SymbolResolver.Resolve(sField);
fieldHandler?.storeField?.Invoke(declaringType, field);
result.inspectorModified = true;
} catch (Exception e) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.AddInspectorField), new EditorExtraData {
{ StatKey.PatchId, patchId },
{ StatKey.Detailed_Exception, e.ToString() },
}).Forget();
Log.Warning($"Failed adding field {sField.fieldName}:{sField.declaringType.typeName} to the inspector. Field will not be displayed. Exception: {e.Message}");
}
}
result.addedFields.AddRange(sFields);
}
// IMPORTANT: must come before HandleNewFields. Might contain new fields which we don't want to hide
internal void HandleAlteredFields(string patchId, RegisterPatchesResult result, SField[] alteredFields) {
if (alteredFields == null) {
return;
}
bool alteredFieldHidden = false;
foreach(var sField in alteredFields) {
try {
var declaringType = SymbolResolver.Resolve(sField.declaringType);
if (fieldHandler?.hideField?.Invoke(declaringType, sField.fieldName) == true) {
alteredFieldHidden = true;
}
} catch(Exception e) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.HideInspectorField), new EditorExtraData {
{ StatKey.PatchId, patchId },
{ StatKey.Detailed_Exception, e.ToString() },
}).Forget();
Log.Warning($"Failed hiding field {sField.fieldName}:{sField.declaringType.typeName} from the inspector. Exception: {e.Message}");
}
}
if (alteredFieldHidden) {
result.inspectorModified = true;
}
}
#endif
Dictionary<MethodBase, MethodBase> previousPatchMethods = new Dictionary<MethodBase, MethodBase>();
public IEnumerable<MethodBase> OriginalPatchMethods => previousPatchMethods.Keys;
List<MethodBase> newMethods = new List<MethodBase>();
string PatchMethod(Module module, SMethod sOriginalMethod, SMethod sPatchMethod, bool containsBurstJobs, RegisterPatchesResult patchesResult) {
string PatchMethod(string patchId, SMethod sOriginalMethod, SMethod sPatchMethod, bool containsBurstJobs, RegisterPatchesResult patchesResult) {
try {
var patchMethod = module.ResolveMethod(sPatchMethod.metadataToken);
var patchMethod = SymbolResolver.Resolve(sPatchMethod);
var start = DateTime.UtcNow;
var state = TryResolveMethod(sOriginalMethod, patchMethod);
if (Debugger.IsAttached && !debuggerCompatibilityEnabled) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.DebuggerAttached), new EditorExtraData {
{ StatKey.PatchId, patchId },
}).Forget();
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.";
}
if (DateTime.UtcNow - start > TimeSpan.FromMilliseconds(500)) {
Log.Info("Hot Reload apply took {0}", (DateTime.UtcNow - start).TotalMilliseconds);
}
if(state.match == null) {
var error =
"Method mismatch: {0}, patch: {1}. This can have multiple reasons:\n"
+ "1. You are running the Editor multiple times for the same project using symlinks, and are making changes from the symlink project\n"
+ "2. A bug in Hot Reload. Please send us a reproduce (code before/after), and we'll get it fixed for you\n"
;
Log.Warning(error, sOriginalMethod.simpleName, patchMethod.Name);
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.";
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.MethodMismatch), new EditorExtraData {
{ StatKey.PatchId, patchId },
}).Forget();
return string.Format(error, sOriginalMethod.simpleName, patchMethod.Name);
}
@@ -239,10 +509,18 @@ namespace SingularityGroup.HotReload {
//ignore. The method is likely burst compiled and can't be patched
return null;
} else {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.Failure), new EditorExtraData {
{ StatKey.PatchId, patchId },
{ StatKey.Detailed_Exception, result.exception.ToString() },
}).Forget();
return HandleMethodPatchFailure(sOriginalMethod, result.exception);
}
}
} catch(Exception ex) {
RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Patching, StatEventType.Exception), new EditorExtraData {
{ StatKey.PatchId, patchId },
{ StatKey.Detailed_Exception, ex.ToString() },
}).Forget();
return HandleMethodPatchFailure(sOriginalMethod, ex);
}
}
@@ -316,7 +594,11 @@ namespace SingularityGroup.HotReload {
MethodBase resolvedMethod = null;
try {
resolvedMethod = TryGetMethodBaseWithRelativeToken(methodToResolve, offset);
if(!MethodCompatiblity.AreMethodsCompatible(resolvedMethod, patchMethod)) {
var err = MethodCompatiblity.CheckCompatibility(resolvedMethod, patchMethod);
if(err != null) {
// if (resolvedMethod.Name == patchMethod.Name) {
// Log.Info(err);
// }
resolvedMethod = null;
}
} catch (SymbolResolvingFailedException ex) when(ex.InnerException is ArgumentOutOfRangeException) {
@@ -331,15 +613,11 @@ namespace SingularityGroup.HotReload {
return symbolResolver.Resolve(new SMethod(sOriginalMethod.assemblyName,
sOriginalMethod.displayName,
sOriginalMethod.metadataToken + offset,
sOriginalMethod.genericTypeArguments,
sOriginalMethod.genericTypeArguments,
sOriginalMethod.simpleName));
}
string HandleMethodPatchFailure(SMethod method, Exception exception) {
var err = $"Failed to apply patch for method {method.displayName} in assembly {method.assemblyName}\n{exception}";
Log.Warning(err);
return err;
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}";
}
void EnsureSymbolResolver() {

View File

@@ -38,12 +38,12 @@ RenderSettings:
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_IndirectSpecularColor: {r: 0.18028378, g: 0.22571412, b: 0.30692285, a: 1}
m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
serializedVersion: 11
m_GIWorkflowMode: 0
m_GISettings:
serializedVersion: 2
@@ -98,13 +98,13 @@ LightmapSettings:
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 4890085278179872738, guid: 463b4a464af955e4d8d6b0a2923d94d0, type: 2}
m_UseShadowmask: 1
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
serializedVersion: 2
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
@@ -117,9 +117,7 @@ NavMeshSettings:
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
accuratePlacement: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
@@ -150,18 +148,10 @@ BoxCollider:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 19295889}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 1, y: 1, z: 1}
serializedVersion: 2
m_Size: {x: 2, y: 2, z: 2}
m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &19295891
MeshRenderer:
@@ -174,12 +164,10 @@ MeshRenderer:
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@@ -204,7 +192,6 @@ MeshRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &19295892
MeshFilter:
m_ObjectHideFlags: 0
@@ -223,7 +210,6 @@ Transform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 4
@@ -268,17 +254,9 @@ Camera:
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_FocalLength: 50
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
@@ -315,7 +293,6 @@ Transform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
@@ -348,7 +325,6 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 511172213}
m_RootOrder: 0
@@ -373,7 +349,6 @@ MonoBehaviour:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
@@ -429,7 +404,6 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 460271677}
m_Father: {fileID: 649153321}
@@ -454,7 +428,6 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -499,7 +472,6 @@ MonoBehaviour:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
@@ -580,7 +552,6 @@ MonoBehaviour:
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!223 &649153320
Canvas:
m_ObjectHideFlags: 0
@@ -599,7 +570,6 @@ Canvas:
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_AdditionalShaderChannelsFlag: 0
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
@@ -613,7 +583,6 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1537524790}
- {fileID: 1847025553}
@@ -656,7 +625,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
@@ -689,7 +657,6 @@ Transform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 5
@@ -770,7 +737,6 @@ Light:
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ShadowRadius: 0
m_ShadowAngle: 0
--- !u!4 &965437872
@@ -783,7 +749,6 @@ Transform:
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 1
@@ -816,7 +781,6 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1847025553}
m_RootOrder: 0
@@ -841,7 +805,6 @@ MonoBehaviour:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
@@ -896,7 +859,6 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 649153321}
m_RootOrder: 0
@@ -921,7 +883,6 @@ MonoBehaviour:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
@@ -977,7 +938,6 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1101930859}
m_Father: {fileID: 649153321}
@@ -1002,7 +962,6 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -1047,7 +1006,6 @@ MonoBehaviour:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
@@ -1097,7 +1055,6 @@ Transform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 18.716805, y: 53.419094, z: 92.546875}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 2
@@ -1119,3 +1076,4 @@ MonoBehaviour:
openWindowButton: {fileID: 1847025554}
openScriptButton: {fileID: 511172214}
thisScript: {fileID: 11500000, guid: 5a2e4d3f095a9441688c70278068eee0, type: 3}
myEditedField: 54

View File

@@ -8,24 +8,27 @@ using UnityEngine.UI;
namespace SingularityGroup.HotReload.Demo {
class HotReloadBasicDemo : MonoBehaviour {
public GameObject cube;
public Text informationText;
public Button openWindowButton;
public Button openScriptButton;
public TextAsset thisScript;
// // 1. Adding fields (Added fields can show in the inspector)
// public int myNewField = 1;
void Start() {
if(Application.isEditor) {
if (Application.isEditor) {
openWindowButton.onClick.AddListener(Demo.I.OpenHotReloadWindow);
openScriptButton.onClick.AddListener(() => Demo.I.OpenScriptFile(thisScript, 31, 13));
openScriptButton.onClick.AddListener(() => Demo.I.OpenScriptFile(thisScript, myStaticField, 13));
} else {
openWindowButton.gameObject.SetActive(false);
openScriptButton.gameObject.SetActive(false);
informationText.gameObject.SetActive(false);
}
}
// Update is called once per frame
void Update() {
if (Demo.I.IsServerRunning()) {
@@ -33,25 +36,25 @@ namespace SingularityGroup.HotReload.Demo {
} else {
informationText.text = "Hot Reload is not running";
}
// // 1. Editing functions in monobehaviours, normal classes or static classes
// // 2. Editing functions in monobehaviours, normal classes or static classes
// // Edit the vector to rotate the cube in the scene differently or change the speed
// var speed = 100;
// cube.transform.Rotate(new Vector3(0, 1, 0) * Time.deltaTime * speed);
// // 1. Editing functions in monobehaviours, normal classes or static classes
// // 2. Editing functions in monobehaviours, normal classes or static classes
// // Uncomment this code to scale the cube
// cube.transform.localScale = Mathf.Sin(Time.time) * Vector3.one;
// // 1. Editing functions in monobehaviours, normal classes or static classes
// // 2. Editing functions in monobehaviours, normal classes or static classes
// // Uncomment this code to make the cube move from left to right and back
// var newPos = cube.transform.position += (cube.transform.localScale.x < 0.5 ? Vector3.left : Vector3.right) * Time.deltaTime;
// if(Mathf.Abs(newPos.x) > 10) {
// cube.transform.position = Vector3.zero;
// }
}
// 2. Editing lambda methods
// 3. Editing lambda methods
static Func<int, int> addFunction = x => {
var result = x + 10;
Debug.Log("Add: " + result);
@@ -60,17 +63,17 @@ namespace SingularityGroup.HotReload.Demo {
// Debug.Log("Multiply: " + result);
return result;
};
// 3. Editing async/await methods
// 4. Editing async/await methods
async Task AsyncMethod() {
// await Task.Delay(500);
// Debug.Log("AsyncMethod");
// // silicense warning
await Task.CompletedTask;
}
// 4. Editing properties (get/set)
// 5. Editing properties (get/set)
public static string SomeString {
// edit the get method
get {
@@ -78,8 +81,8 @@ namespace SingularityGroup.HotReload.Demo {
return someStringHere;
}
}
// 5. Editing indexers (square bracket access such as dictionaries)
// 6. Editing indexers (square bracket access such as dictionaries)
class CustomDictionary : Dictionary<string, int> {
public new int this[string key] {
get {
@@ -102,8 +105,8 @@ namespace SingularityGroup.HotReload.Demo {
{ "d", 19 },
{ "D", 20 }
};
// 6. Editing operators methods (explicit and implicit operators)
// 7. Editing operators methods (explicit and implicit operators)
public class Email {
public string Value { get; }
@@ -116,7 +119,7 @@ namespace SingularityGroup.HotReload.Demo {
// Uncomment to change the implicit operator
// => value.Value + " FOO";
=> value.Value;
// // Uncomment to change add an implicit operator
// public static implicit operator byte[](Email value)
// => Encoding.UTF8.GetBytes(value.Value);
@@ -126,51 +129,86 @@ namespace SingularityGroup.HotReload.Demo {
=> new Email(value);
}
// 8. Editing fields: modifiers/type/name/initializer
public int myEditedField = 4;
// 9. Editing static field initializers (variable value is updated)
static readonly int myStaticField = 31;
// // 10. Adding auto properties/events
// int MyProperty { get; set; } = 6;
// event Action MyEvent = () => Debug.Log("MyEvent");
class GenericClass<T> {
// // 11. Adding methods in generic classes
// public void GenericMethod() {
// Debug.Log("GenericMethod");
// }
// // 12. Adding fields (any type) in generic classes
// public T myGenericField;
}
void LateUpdate() {
// // 2. Editing lambda methods
// // 3. Editing lambda methods
// addFunction(10);
// // 3. Editing async/await methods
// // 4. Editing async/await methods
// AsyncMethod().Forget();
// // 4. Editing properties (get/set)
// // 5. Editing properties (get/set)
// Debug.Log(SomeString);
// // 5. Editing indexers (square bracket access such as dictionaries)
// // 6. Editing indexers (square bracket access such as dictionaries)
// Debug.Log(randomDict["A"]);
// // 6. Editing operators methods (explicit and implicit operators)
// // 7. Editing operators methods (explicit and implicit operators)
Email email = new Email("example@example.com");
// string stringEmail = email;
// Debug.Log(stringEmail);
// // Uncomment new operator in Email class + Uncomment this to add byte implicit operator
// byte[] byteEmail = email;
// var hexRepresentation = BitConverter.ToString(byteEmail);
// Debug.Log(hexRepresentation);
// Debug.Log(Encoding.UTF8.GetString(byteEmail));
// // 8. Editing fields: modifiers/type/name/initializer
// Debug.Log("myEditedField: " + myEditedField);
// // 9. Editing static field initializers (variable value is updated)
// Debug.Log("myStaticField: " + myStaticField);
// // 10. Adding auto properties/events
// Debug.Log("MyProperty: " + MyProperty);
// MyEvent.Invoke();
// // 7. Editing lambda methods with closures
// var newClass = new GenericClass<int>();
// // 11. Adding methods in generic classes
// newClass.GenericMethod();
// // 12. Adding fields in generic classes
// newClass.myGenericField = 3;
// Debug.Log("myGenericField: " + newClass.myGenericField);
// // 13. Editing lambda methods with closures
// // Uncomment to log sorted array
// // Switch a and b to reverse the sorting
// int[] numbers = { 5, 3, 8, 1, 9 };
// Array.Sort(numbers, (b, a) => a.CompareTo(b));
// Debug.Log(string.Join(", ", numbers));
}
// This function gets invoked every time it's patched
[InvokeOnHotReloadLocal]
static void OnHotReloadMe() {
// change the string to see the method getting invoked
Debug.Log("Hello there");
// // change the string to see the method getting invoked
// Debug.Log("Hello there");
}
// // 8. Adding event functions
// // 14. Adding event functions
// void OnDisable() {
// Debug.Log("OnDisable");
// }

View File

@@ -6,7 +6,7 @@ using SingularityGroup.HotReload.MonoMod.Utils;
namespace SingularityGroup.HotReload {
static class MethodCompatiblity {
internal static bool AreMethodsCompatible(MethodBase previousMethod, MethodBase patchMethod) {
internal static string CheckCompatibility(MethodBase previousMethod, MethodBase patchMethod) {
var previousConstructor = previousMethod as ConstructorInfo;
var patchConstructor = patchMethod as ConstructorInfo;
if(previousConstructor != null && !ReferenceEquals(patchConstructor, null)) {
@@ -17,20 +17,29 @@ namespace SingularityGroup.HotReload {
if(!ReferenceEquals(previousMethodInfo, null) && !ReferenceEquals(patchMethodInfo, null)) {
return AreMethodInfosCompatible(previousMethodInfo, patchMethodInfo);
}
return false;
return "unknown issue";
}
static bool AreMethodBasesCompatible(MethodBase previousMethod, MethodBase patchMethod) {
static string AreMethodBasesCompatible(MethodBase previousMethod, MethodBase patchMethod) {
if(previousMethod.Name != patchMethod.Name) {
return false;
return "Method name mismatch";
}
//Declaring type of patch method is different from the target method but their full name (namespace + name) is equal
if(previousMethod.DeclaringType.FullName != patchMethod.DeclaringType.FullName) {
return false;
bool isDeclaringTypeCompatible = false;
var declaringType = patchMethod.DeclaringType;
while (declaringType != null) {
if(previousMethod.DeclaringType?.FullName == declaringType.FullName) {
isDeclaringTypeCompatible = true;
break;
}
declaringType = declaringType.BaseType;
}
if (!isDeclaringTypeCompatible) {
return "Declaring type name mismatch";
}
//Check in case type parameter overloads to distinguish between: void M<T>() { } <-> void M() { }
if(previousMethod.IsGenericMethodDefinition != patchMethod.IsGenericMethodDefinition) {
return false;
return "IsGenericMethodDefinition mismatch";
}
var prevParams = previousMethod.GetParameters();
@@ -53,11 +62,11 @@ namespace SingularityGroup.HotReload {
//Special case: patch method for an instance method is static and has an explicit this parameter.
//If the patch method doesn't have any parameters it is not compatible.
if(patchParams.Length == 0) {
return false;
return "missing this parameter";
}
//this parameter has to be the declaring type
if(!ParamTypeMatches(patchParams[0].ParameterType, previousMethod.DeclaringType)) {
return false;
return "this parameter type mismatch";
}
//Ignore the this parameter and compare the remaining ones.
patchParamsSegment = new ArraySegment<ParameterInfo>(patchParams, 1, patchParams.Length - 1);
@@ -75,9 +84,6 @@ namespace SingularityGroup.HotReload {
if (!ParamTypeMatches(patchT, previousMethod.DeclaringType)) {
return false;
}
if (prevParams.Length >= 1 && prevParams[0].ParameterType == previousMethod.DeclaringType) {
return false;
}
return patchParams[0].Name == "this";
}
@@ -85,25 +91,25 @@ namespace SingularityGroup.HotReload {
return patchT == originalT || patchT.IsByRef && patchT.GetElementType() == originalT;
}
static bool CompareParameters(ArraySegment<ParameterInfo> x, ArraySegment<ParameterInfo> y) {
static string CompareParameters(ArraySegment<ParameterInfo> x, ArraySegment<ParameterInfo> y) {
if(x.Count != y.Count) {
return false;
return "parameter count mismatch";
}
for (var i = 0; i < x.Count; i++) {
if(x.Array[i + x.Offset].ParameterType != y.Array[i + y.Offset].ParameterType) {
return false;
return "parameter type mismatch";
}
}
return true;
return null;
}
static bool AreConstructorsCompatible(ConstructorInfo x, ConstructorInfo y) {
static string AreConstructorsCompatible(ConstructorInfo x, ConstructorInfo y) {
return AreMethodBasesCompatible(x, y);
}
static bool AreMethodInfosCompatible(MethodInfo x, MethodInfo y) {
return AreMethodBasesCompatible(x, y) && x.ReturnType == y.ReturnType;
static string AreMethodInfosCompatible(MethodInfo x, MethodInfo y) {
return AreMethodBasesCompatible(x, y) ?? (x.ReturnType == y.ReturnType ? null : "Return type mismatch");
}
}
}

View File

@@ -30,6 +30,11 @@ namespace SingularityGroup.HotReload.JsonConverters {
CodePatch[] patches = null;
string[] failures = null;
SMethod[] removedMethod = null;
SField[] alteredFields = null;
SField[] addedFieldInitializerFields = null;
SMethod[] addedFieldInitializerInitializers = null;
SField[] removedFieldInitializers = null;
SField[] newFieldDefinitions = null;
while (reader.Read()) {
if (reader.TokenType == JsonToken.EndObject) {
@@ -56,6 +61,26 @@ namespace SingularityGroup.HotReload.JsonConverters {
case nameof(MethodPatchResponse.removedMethod):
removedMethod = ReadSMethodArray(reader);
break;
case nameof(MethodPatchResponse.alteredFields):
alteredFields = ReadSFields(reader);
break;
case nameof(MethodPatchResponse.addedFieldInitializerFields):
addedFieldInitializerFields = ReadSFields(reader);
break;
case nameof(MethodPatchResponse.addedFieldInitializerInitializers):
addedFieldInitializerInitializers = ReadSMethodArray(reader);
break;
case nameof(MethodPatchResponse.removedFieldInitializers):
removedFieldInitializers = ReadSFields(reader);
break;
case nameof(MethodPatchResponse.newFieldDefinitions):
newFieldDefinitions = ReadSFields(reader);
break;
default:
reader.Skip(); // Skip unknown properties
@@ -68,8 +93,14 @@ namespace SingularityGroup.HotReload.JsonConverters {
patches ?? Array.Empty<CodePatch>(),
failures ?? Array.Empty<string>(),
removedMethod ?? Array.Empty<SMethod>(),
// Note: doesn't have to be persisted here
Array.Empty<PartiallySupportedChange>()
alteredFields ?? Array.Empty<SField>(),
// Note: suggestions don't have to be persisted here
Array.Empty<PartiallySupportedChange>(),
Array.Empty<HotReloadSuggestionKind>(),
addedFieldInitializerFields ?? Array.Empty<SField>(),
addedFieldInitializerInitializers ?? Array.Empty<SMethod>(),
removedFieldInitializers ?? Array.Empty<SField>(),
newFieldDefinitions ?? Array.Empty<SField>()
);
}
@@ -91,6 +122,12 @@ namespace SingularityGroup.HotReload.JsonConverters {
SMethod[] patchMethods = null;
SMethod[] newMethods = null;
SUnityJob[] unityJobs = null;
SField[] newFields = null;
SField[] deletedFields = null;
SField[] renamedFieldsFrom = null;
SField[] renamedFieldsTo = null;
SField[] propertyAttributesFieldOriginal = null;
SField[] propertyAttributesFieldUpdated = null;
while (reader.Read()) {
if (reader.TokenType == JsonToken.EndObject) {
@@ -133,6 +170,29 @@ namespace SingularityGroup.HotReload.JsonConverters {
case nameof(CodePatch.unityJobs):
unityJobs = ReadSUnityJobArray(reader);
break;
case nameof(CodePatch.newFields):
newFields = ReadSFields(reader);
break;
case nameof(CodePatch.deletedFields):
deletedFields = ReadSFields(reader);
break;
case nameof(CodePatch.renamedFieldsFrom):
renamedFieldsFrom = ReadSFields(reader);
break;
case nameof(CodePatch.renamedFieldsTo):
renamedFieldsTo = ReadSFields(reader);
break;
case nameof(CodePatch.propertyAttributesFieldOriginal):
propertyAttributesFieldOriginal = ReadSFields(reader);
break;
case nameof(CodePatch.propertyAttributesFieldUpdated):
propertyAttributesFieldUpdated = ReadSFields(reader);
break;
default:
reader.Skip(); // Skip unknown properties
@@ -148,7 +208,13 @@ namespace SingularityGroup.HotReload.JsonConverters {
modifiedMethods: modifiedMethods ?? Array.Empty<SMethod>(),
patchMethods: patchMethods ?? Array.Empty<SMethod>(),
newMethods: newMethods ?? Array.Empty<SMethod>(),
unityJobs: unityJobs ?? Array.Empty<SUnityJob>()
unityJobs: unityJobs ?? Array.Empty<SUnityJob>(),
newFields: newFields ?? Array.Empty<SField>(),
deletedFields: deletedFields ?? Array.Empty<SField>(),
renamedFieldsFrom: renamedFieldsFrom ?? Array.Empty<SField>(),
renamedFieldsTo: renamedFieldsTo ?? Array.Empty<SField>(),
propertyAttributesFieldOriginal: propertyAttributesFieldOriginal ?? Array.Empty<SField>(),
propertyAttributesFieldUpdated: propertyAttributesFieldUpdated ?? Array.Empty<SField>()
));
}
@@ -210,13 +276,23 @@ namespace SingularityGroup.HotReload.JsonConverters {
return array.ToArray();
}
private SField[] ReadSFields(JsonReader reader) {
var array = new List<SField>();
while (reader.Read()) {
if (reader.TokenType == JsonToken.StartObject) {
array.Add(ReadSField(reader));
} else if (reader.TokenType == JsonToken.EndArray) {
break; // End of the SUnityJob array
}
}
return array.ToArray();
}
private SMethod ReadSMethod(JsonReader reader) {
string assemblyName = null;
string displayName = null;
int metadataToken = default(int);
SType[] genericTypeArguments = null;
SType[] genericArguments = null;
string simpleName = null;
while (reader.Read()) {
@@ -241,14 +317,6 @@ namespace SingularityGroup.HotReload.JsonConverters {
metadataToken = reader.ReadAsInt32() ?? default(int);
break;
case nameof(SMethod.genericTypeArguments):
genericTypeArguments = ReadSTypeArray(reader);
break;
case nameof(SMethod.genericArguments):
genericArguments = ReadSTypeArray(reader);
break;
case nameof(SMethod.simpleName):
simpleName = reader.ReadAsString();
break;
@@ -263,8 +331,6 @@ namespace SingularityGroup.HotReload.JsonConverters {
assemblyName ?? string.Empty,
displayName ?? string.Empty,
metadataToken,
genericTypeArguments ?? Array.Empty<SType>(),
genericArguments ?? Array.Empty<SType>(),
simpleName ?? string.Empty
);
}
@@ -272,9 +338,12 @@ namespace SingularityGroup.HotReload.JsonConverters {
private SType ReadSType(JsonReader reader) {
string assemblyName = null;
string typeName = null;
SType[] genericArguments = null;
int? metadataToken = null;
while (reader.Read()) {
if (reader.TokenType == JsonToken.Null) {
return null;
}
if (reader.TokenType == JsonToken.EndObject) {
break;
}
@@ -292,8 +361,8 @@ namespace SingularityGroup.HotReload.JsonConverters {
typeName = reader.ReadAsString();
break;
case nameof(SType.genericArguments):
genericArguments = ReadSTypeArray(reader);
case nameof(SType.metadataToken):
metadataToken = reader.ReadAsInt32();
break;
default:
@@ -305,7 +374,7 @@ namespace SingularityGroup.HotReload.JsonConverters {
return new SType(
assemblyName ?? string.Empty,
typeName ?? string.Empty,
genericArguments ?? Array.Empty<SType>()
metadataToken ?? 0
);
}
@@ -340,6 +409,57 @@ namespace SingularityGroup.HotReload.JsonConverters {
return new SUnityJob(metadataToken, jobKind);
}
private SField ReadSField(JsonReader reader) {
SType declaringType = null;
string fieldName = null;
string assemblyName = null;
int? metadataToken = null;
bool? serializable = null;
bool? isStatic = null;
while (reader.Read()) {
if (reader.TokenType == JsonToken.EndObject) {
break;
}
if (reader.TokenType != JsonToken.PropertyName) {
continue;
}
var propertyName = (string)reader.Value;
switch (propertyName) {
case nameof(SField.declaringType):
declaringType = ReadSType(reader);
break;
case nameof(SField.fieldName):
fieldName = reader.ReadAsString();
break;
case nameof(SField.assemblyName):
assemblyName = reader.ReadAsString();
break;
case nameof(SField.metadataToken):
metadataToken = reader.ReadAsInt32();
break;
case nameof(SField.serializable):
serializable = reader.ReadAsBoolean();
break;
case nameof(SField.isStatic):
isStatic = reader.ReadAsBoolean();
break;
default:
reader.Skip(); // Skip unknown properties
break;
}
}
return new SField(declaringType: declaringType, fieldName: fieldName, assemblyName: assemblyName, metadataToken ?? 0, isStatic ?? false, serializable ?? false);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
var responses = (List<MethodPatchResponse>)value;
@@ -412,6 +532,60 @@ namespace SingularityGroup.HotReload.JsonConverters {
}
writer.WriteEndArray();
}
if (responsePatch.newFields != null) {
writer.WritePropertyName(nameof(responsePatch.newFields));
writer.WriteStartArray();
foreach (var newField in responsePatch.newFields) {
WriteSField(writer, newField);
}
writer.WriteEndArray();
}
if (responsePatch.deletedFields != null) {
writer.WritePropertyName(nameof(responsePatch.deletedFields));
writer.WriteStartArray();
foreach (var deletedField in responsePatch.deletedFields) {
WriteSField(writer, deletedField);
}
writer.WriteEndArray();
}
if (responsePatch.renamedFieldsFrom != null) {
writer.WritePropertyName(nameof(responsePatch.renamedFieldsFrom));
writer.WriteStartArray();
foreach (var removedFieldFrom in responsePatch.renamedFieldsFrom) {
WriteSField(writer, removedFieldFrom);
}
writer.WriteEndArray();
}
if (responsePatch.renamedFieldsTo != null) {
writer.WritePropertyName(nameof(responsePatch.renamedFieldsTo));
writer.WriteStartArray();
foreach (var removedFieldTo in responsePatch.renamedFieldsTo) {
WriteSField(writer, removedFieldTo);
}
writer.WriteEndArray();
}
if (responsePatch.propertyAttributesFieldOriginal != null) {
writer.WritePropertyName(nameof(responsePatch.propertyAttributesFieldOriginal));
writer.WriteStartArray();
foreach (var removedFieldFrom in responsePatch.propertyAttributesFieldOriginal) {
WriteSField(writer, removedFieldFrom);
}
writer.WriteEndArray();
}
if (responsePatch.propertyAttributesFieldUpdated != null) {
writer.WritePropertyName(nameof(responsePatch.propertyAttributesFieldUpdated));
writer.WriteStartArray();
foreach (var removedFieldTo in responsePatch.propertyAttributesFieldUpdated) {
WriteSField(writer, removedFieldTo);
}
writer.WriteEndArray();
}
writer.WriteEndObject();
}
@@ -436,6 +610,51 @@ namespace SingularityGroup.HotReload.JsonConverters {
writer.WriteEndArray();
}
if (response.alteredFields != null) {
writer.WritePropertyName(nameof(response.alteredFields));
writer.WriteStartArray();
foreach (var alteredField in response.alteredFields) {
WriteSField(writer, alteredField);
}
writer.WriteEndArray();
}
if (response.addedFieldInitializerFields != null) {
writer.WritePropertyName(nameof(response.addedFieldInitializerFields));
writer.WriteStartArray();
foreach (var addedFieldInitializerField in response.addedFieldInitializerFields) {
WriteSField(writer, addedFieldInitializerField);
}
writer.WriteEndArray();
}
if (response.addedFieldInitializerInitializers != null) {
writer.WritePropertyName(nameof(response.addedFieldInitializerInitializers));
writer.WriteStartArray();
foreach (var addedFieldInitializerInitializer in response.addedFieldInitializerInitializers) {
WriteSMethod(writer, addedFieldInitializerInitializer);
}
writer.WriteEndArray();
}
if (response.removedFieldInitializers != null) {
writer.WritePropertyName(nameof(response.removedFieldInitializers));
writer.WriteStartArray();
foreach (var removedFieldInitializer in response.removedFieldInitializers) {
WriteSField(writer, removedFieldInitializer);
}
writer.WriteEndArray();
}
if (response.newFieldDefinitions != null) {
writer.WritePropertyName(nameof(response.newFieldDefinitions));
writer.WriteStartArray();
foreach (var newFieldDefinition in response.newFieldDefinitions) {
WriteSField(writer, newFieldDefinition);
}
writer.WriteEndArray();
}
writer.WriteEndObject();
}
writer.WriteEndArray();
@@ -450,51 +669,48 @@ namespace SingularityGroup.HotReload.JsonConverters {
writer.WriteValue(method.displayName);
writer.WritePropertyName(nameof(method.metadataToken));
writer.WriteValue(method.metadataToken);
if (method.genericTypeArguments != null) {
writer.WritePropertyName(nameof(method.genericTypeArguments));
writer.WriteStartArray();
foreach (var genericTypeArgument in method.genericTypeArguments) {
WriteSType(writer, genericTypeArgument);
}
writer.WriteEndArray();
}
if (method.genericArguments != null) {
writer.WritePropertyName(nameof(method.genericArguments));
writer.WriteStartArray();
foreach (var genericArgument in method.genericArguments) {
WriteSType(writer, genericArgument);
}
writer.WriteEndArray();
}
writer.WritePropertyName(nameof(method.simpleName));
writer.WriteValue(method.simpleName);
writer.WriteEndObject();
}
void WriteSField(JsonWriter writer, SField field) {
writer.WriteStartObject();
writer.WritePropertyName(nameof(field.declaringType));
writer.WriteSType(field.declaringType);
writer.WritePropertyName(nameof(field.fieldName));
writer.WriteValue(field.fieldName);
writer.WritePropertyName(nameof(field.assemblyName));
writer.WriteValue(field.assemblyName);
writer.WritePropertyName(nameof(field.metadataToken));
writer.WriteValue(field.metadataToken);
writer.WritePropertyName(nameof(field.serializable));
writer.WriteValue(field.serializable);
writer.WriteEndObject();
}
void WriteSType(JsonWriter writer, SType type) {
}
internal static class MethodPatchResponsesConverterExtensions {
public static void WriteSType(this JsonWriter writer, SType type) {
if (type == null) {
writer.WriteNull();
return;
}
writer.WriteStartObject();
writer.WritePropertyName(nameof(type.assemblyName));
writer.WriteValue(type.assemblyName);
writer.WritePropertyName(nameof(type.typeName));
writer.WriteValue(type.typeName);
// always writing generic arguments will cause recursion issues
if (type.genericArguments?.Length > 0) {
writer.WritePropertyName(nameof(type.genericArguments));
writer.WriteStartArray();
foreach (var genericArgument in type.genericArguments) {
WriteSType(writer, genericArgument);
}
writer.WriteEndArray();
}
writer.WritePropertyName(nameof(type.metadataToken));
writer.WriteValue(type.metadataToken);
writer.WriteEndObject();
}
}
}
#endif

View File

@@ -14,8 +14,21 @@ namespace SingularityGroup.HotReload {
ptr->monoMethodFlags |= Interop.MonoMethodFlags.skip_visibility;
}
}
public static unsafe bool IsMethodInlined(MethodBase method) {
if(IntPtr.Size == sizeof(long)) {
var ptr = (Interop.MonoMethod64*)method.MethodHandle.Value.ToPointer();
return (ptr -> monoMethodFlags & Interop.MonoMethodFlags.inline_info) == Interop.MonoMethodFlags.inline_info;
} else {
var ptr = (Interop.MonoMethod32*)method.MethodHandle.Value.ToPointer();
return (ptr -> monoMethodFlags & Interop.MonoMethodFlags.inline_info) == Interop.MonoMethodFlags.inline_info;
}
}
#else
public static void DisableVisibilityChecks(MethodBase method) { }
public static bool IsMethodInlined(MethodBase method) {
return false;
}
#endif
}
}

View File

@@ -4,6 +4,9 @@ using System.Collections;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.EventSystems;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
namespace SingularityGroup.HotReload {
internal class Prompts : MonoBehaviour {
@@ -105,10 +108,18 @@ namespace SingularityGroup.HotReload {
private void Update() {
if (!userTriedToInteract) {
// when user interacts with the screen, make sure overlay can handle taps
#if ENABLE_INPUT_SYSTEM
if ((Touchscreen.current != null && Touchscreen.current.touches.Count > 0) ||
(Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame)) {
userTriedToInteract = true;
DoEnsureEventSystem();
}
#else
if (Input.touchCount > 0 || Input.GetMouseButtonDown(0)) {
userTriedToInteract = true;
DoEnsureEventSystem();
}
#endif
}
}

View File

@@ -8,10 +8,10 @@ namespace SingularityGroup.HotReload {
public static bool IsAssetStoreBuild => true;
public const string Version = "1.12.14";
public const string Version = "1.13.7";
// Never higher than Version
// Used for the download
public const string ServerVersion = "1.12.12";
public const string ServerVersion = "1.13.7";
public const string PackageName = "com.singularitygroup.hotreload";
public const string LibraryCachePath = "Library/" + PackageName;
public const string ConfigFileName = "hot-reload-config.json";

View File

@@ -100,7 +100,6 @@ namespace SingularityGroup.HotReload {
static void HandleResponseReceived(MethodPatchResponse response) {
Log.Debug("PollMethodPatches handling MethodPatchResponse id:{0} response.patches.Length:{1} response.failures.Length:{2}",
response.id, response.patches.Length, response.failures.Length);
// TODO handle new response data (removed methods etc.)
if(response.patches.Length > 0) {
CodePatcher.I.RegisterPatches(response, persist: true);
}

View File

@@ -66,6 +66,8 @@ namespace SingularityGroup.HotReload {
return;
}
CodePatcher.I.debuggerCompatibilityEnabled = true;
try {
var customIp = PlayerPrefs.GetString("HotReloadRuntime.CustomIP", "");
if (!string.IsNullOrEmpty(customIp)) {

View File

@@ -345,14 +345,41 @@ namespace SingularityGroup.HotReload {
}
}
public static bool IsReleaseMode() {
# if (UNITY_EDITOR && UNITY_2022_1_OR_NEWER)
return UnityEditor.Compilation.CompilationPipeline.codeOptimization == UnityEditor.Compilation.CodeOptimization.Release;
# elif (UNITY_EDITOR)
return false;
# elif (DEBUG)
return false;
# else
return true;
#endif
}
public static Task RequestClearPatches() {
var body = SerializeRequestBody(new CompileRequest(serverInfo.rootPath));
var body = SerializeRequestBody(new CompileRequest(serverInfo.rootPath, IsReleaseMode()));
return PostJson(url + "/clearpatches", body, 10);
}
public static Task RequestCompile() {
var body = SerializeRequestBody(new CompileRequest(serverInfo.rootPath));
return PostJson(url + "/compile", body, 10);
public static async Task RequestCompile(Action<string> onResponseReceived) {
var body = SerializeRequestBody(new CompileRequest(serverInfo.rootPath, IsReleaseMode()));
var result = await PostJson(url + "/compile", body, 10);
if (result.statusCode == HttpStatusCode.OK && !string.IsNullOrEmpty(result.responseText)) {
var responses = JsonConvert.DeserializeObject<List<string>>(result.responseText);
if (responses == null) {
return;
}
await ThreadUtility.SwitchToMainThread();
foreach (var response in responses) {
// Avoid importing assets twice
if (responses.Contains(response + ".meta")) {
Log.Debug($"Ignoring asset change inside Unity: {response}");
continue;
}
onResponseReceived(response);
}
}
}
internal static async Task<List<ChangelogVersion>> FetchChangelog(int timeoutSeconds = 20) {

View File

@@ -6,7 +6,8 @@
"GUID:2bafac87e7f4b9b418d9448d219b01ab",
"GUID:46c537318f0d530469a1df1fafe86c9c",
"GUID:af67dfb2ec5c9c740be50ae4470ed85f",
"SingularityGroup.HotReload.Runtime.Public"
"SingularityGroup.HotReload.Runtime.Public",
"Unity.InputSystem"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@@ -23,25 +23,50 @@ namespace SingularityGroup.HotReload {
assemblies.Add(asm);
}
public Type Resolve(SType t) {
List<Assembly> assemblies;
if (assembliesByName.TryGetValue(t.assemblyName, out assemblies)) {
Type type;
foreach (var assembly in assemblies) {
if ((type = assembly.GetType(t.typeName)) != null) {
if(t.typeName == "System.Array" && t.genericArguments.Length > 0) {
var elementType = Resolve(t.genericArguments[0]);
return elementType.Assembly.GetType(t.genericArguments[0].typeName + "[]");
public Type Resolve(SType t) {
var assmeblies = Resolve(t.assemblyName);
Type result = null;
Exception lastException = null;
for (var i = 0; i < assmeblies.Count; i++) {
try {
result = assmeblies[i].GetLoadedModules()[0].ResolveType(t.metadataToken);
if (t.isGenericParameter) {
if (!result.IsGenericTypeDefinition) {
throw new SymbolResolvingFailedException(t, new ApplicationException("Generic parameter did not resolve to generic type definition"));
}
if(t.genericArguments.Length > 0) {
type = type.MakeGenericType(ResolveTypes(t.genericArguments));
var genericParameters = result.GetGenericArguments();
if (t.genericParameterPosition >= genericParameters.Length) {
throw new SymbolResolvingFailedException(t, new ApplicationException("Generic parameter did not exist on the generic type definition"));
}
return type;
result = genericParameters[t.genericParameterPosition];
}
break;
} catch(Exception ex) {
lastException = ex;
}
}
throw new SymbolResolvingFailedException(t);
if(result == null) {
throw new SymbolResolvingFailedException(t, lastException);
}
return result;
}
public FieldInfo Resolve(SField t) {
var assmeblies = Resolve(t.assemblyName);
FieldInfo result = null;
Exception lastException = null;
for (var i = 0; i < assmeblies.Count; i++) {
try {
result = assmeblies[i].GetLoadedModules()[0].ResolveField(t.metadataToken);
break;
} catch(Exception ex) {
lastException = ex;
}
}
if(result == null) {
throw new SymbolResolvingFailedException(t, lastException);
}
return result;
}
public IReadOnlyList<Assembly> Resolve(string assembly) {
@@ -54,13 +79,11 @@ namespace SingularityGroup.HotReload {
public MethodBase Resolve(SMethod m) {
var assmeblies = Resolve(m.assemblyName);
var genericTypeArgs = ResolveTypes(m.genericTypeArguments);
var genericMethodArgs = ResolveTypes(m.genericArguments);
MethodBase result = null;
Exception lastException = null;
for (var i = 0; i < assmeblies.Count; i++) {
try {
result = assmeblies[i].GetLoadedModules()[0].ResolveMethod(m.metadataToken, genericTypeArgs, genericMethodArgs);
result = assmeblies[i].GetLoadedModules()[0].ResolveMethod(m.metadataToken);
break;
} catch(Exception ex) {
lastException = ex;
@@ -71,20 +94,6 @@ namespace SingularityGroup.HotReload {
}
return result;
}
Type[] ResolveTypes(SType[] sTypes) {
if(sTypes == null) {
return null;
}
if(sTypes.Length == 0) {
return Array.Empty<Type>();
}
var result = new Type[sTypes.Length];
for (int i = 0; i < sTypes.Length; i++) {
result[i] = Resolve(sTypes[i]);
}
return result;
}
}
}
#endif

View File

@@ -7,8 +7,11 @@ namespace SingularityGroup.HotReload {
public SymbolResolvingFailedException(SMethod m, Exception inner)
: base($"Unable to resolve method {m.displayName} in assembly {m.assemblyName}", inner) { }
public SymbolResolvingFailedException(SType t)
: base($"Unable to resolve type with name: {t.typeName} in assembly {t.assemblyName}") { }
public SymbolResolvingFailedException(SType t, Exception inner)
: base($"Unable to resolve type with name: {t.typeName} in assembly {t.assemblyName}", inner) { }
public SymbolResolvingFailedException(SField t, Exception inner)
: base($"Unable to resolve field with name: {t.fieldName} in assembly {t.assemblyName}", inner) { }
}
}
#endif