added hotreload

This commit is contained in:
2025-07-17 19:17:27 +02:00
parent faf4bd1ca9
commit 12e3b85770
380 changed files with 39434 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Collections;
using UnityEngine;
namespace SingularityGroup.HotReload {
class AppCallbackListener : MonoBehaviour {
/// <summary>
/// Reliable on Android and in the editor.
/// </summary>
/// <remarks>
/// On iOS, OnApplicationPause is not called at expected moments
/// if the app has some background modes enabled in PlayerSettings -Troy.
/// </remarks>
public static event Action<bool> onApplicationPause;
/// <summary>
/// Reliable on Android, iOS and in the editor.
/// </summary>
public static event Action<bool> onApplicationFocus;
static AppCallbackListener instance;
public static AppCallbackListener I => instance;
// Must be called early from Unity main thread (before any usages of the singleton I).
public static AppCallbackListener Init() {
if(instance) return instance;
var go = new GameObject("AppCallbackListener");
go.hideFlags |= HideFlags.HideInHierarchy;
DontDestroyOnLoad(go);
return instance = go.AddComponent<AppCallbackListener>();
}
public bool Paused { get; private set; } = false;
public void DelayedQuit(float seconds) {
StartCoroutine(delayedQuitRoutine(seconds));
}
IEnumerator delayedQuitRoutine(float seconds) {
yield return new WaitForSeconds(seconds);
Application.Quit();
}
void OnApplicationPause(bool paused) {
Paused = paused;
onApplicationPause?.Invoke(paused);
}
void OnApplicationFocus(bool playing) {
onApplicationFocus?.Invoke(playing);
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: a989a17330b04c6fb8f91aa41ac14471
timeCreated: 1674216227
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/AppCallbackListener.cs
uploadId: 752503

View File

@@ -0,0 +1,171 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Collections.Generic;
using System.IO;
using JetBrains.Annotations;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Serialization;
namespace SingularityGroup.HotReload {
/// <summary>
/// Information about the Unity Player build.
/// </summary>
/// <remarks>
/// <para>
/// This info is used by the HotReload Server to compile your project in the same way that the Unity Player build was compiled.<br/>
/// For example, when building for Android, Unity sets a bunch of define symbols like UNITY_ANDROID.
/// </para>
/// <para>
/// Information that changes between builds is generated at build-time and put in StreamingAssets/.<br/>
/// This approach means that builds do not need to modify a project file (making file dirty in git). For example,
/// whenever user makes a mono build, the CommitHash changes and we need to regenerate the BuildInfo.
/// </para>
/// </remarks>
[Serializable]
class BuildInfo {
/// <summary>
/// Uniquely identifies the Unity project.
/// </summary>
/// <remarks>
/// Used on-device to check if Hot Reload server is compatible with the Unity project (same project).<br/>
/// When your computer has multiple Unity projects open, each project should provide a different value.<br/>
/// This identifier must also be the same between two different computers that are collaborating on the same project.
///
/// <para>
/// Edge-case: when a user copy pastes an entire Unity project and has both open at once,
/// then it's fine for this identifier to be the same.
/// </para>
/// </remarks>
public string projectIdentifier;
/// <summary>
/// Git commit hash
/// </summary>
/// <remarks>
/// Used to detect that your code is different to when the build was made.
/// </remarks>
public string commitHash;
/// <summary>
/// List of define symbols that were active when this build was made.
/// </summary>
/// <remarks>
/// Separate the symbols with a semi-colon character ';'
/// </remarks>
public string defineSymbols;
/// <summary>
/// A regex of C# project names (*.csproj) to be omitted from compilation.
/// </summary>
/// <example>
/// "MyTests|MyEditorAssembly"
/// </example>
[FormerlySerializedAs("projectExclusionRegex")]
public string projectOmissionRegex;
/// <summary>
/// The computer that made the Android (or Standalone etc) build.<br/>
/// The hostname (ip address) where Hot Reload server would be listening.
/// </summary>
public string buildMachineHostName;
/// <summary>
/// The computer that made the Android (or Standalone etc) build.<br/>
/// The port where Hot Reload server would be listening.
/// </summary>
public int buildMachinePort;
/// <summary>
/// Selected build target in Unity Editor.
/// </summary>
public string activeBuildTarget;
/// <summary>
/// Used to pass in the origin onto the phone which is used to identify the correct server.
/// </summary>
public string buildMachineRequestOrigin;
[JsonIgnore]
public HashSet<string> DefineSymbolsAsHashSet {
get {
var symbols = defineSymbols.Trim().Split(';');
// split on an empty string produces 1 empty string
if (symbols.Length == 1 && symbols[0] == string.Empty) {
return new HashSet<string>();
}
return new HashSet<string>(symbols);
}
}
[JsonIgnore]
public PatchServerInfo BuildMachineServer {
get {
if (buildMachineHostName == null || buildMachinePort == 0) {
return null;
}
return new PatchServerInfo(buildMachineHostName, buildMachinePort, commitHash, null, customRequestOrigin: buildMachineRequestOrigin);
}
}
public string ToJson() {
return JsonConvert.SerializeObject(this);
}
[CanBeNull]
public static BuildInfo FromJson(string json) {
if (string.IsNullOrEmpty(json)) {
return null;
}
return JsonConvert.DeserializeObject<BuildInfo>(json);
}
/// <summary>
/// Path to read/write the json file to.
/// </summary>
/// <returns>A filepath that is inside the player build</returns>
public static string GetStoredPath() {
return Path.Combine(Application.streamingAssetsPath, GetStoredName());
}
public static string GetStoredName() {
return "HotReload_BuildInfo.json";
}
/// <returns>True if the commit hashes are definately different, otherwise False</returns>
public bool IsDifferentCommit(string remoteCommit) {
if (commitHash == PatchServerInfo.UnknownCommitHash) {
return false;
}
return !SameCommit(commitHash, remoteCommit);
}
/// <summary>
/// Checks whether the commits are equivalent.
/// </summary>
/// <param name="commitA"></param>
/// <param name="commitB"></param>
/// <returns>False if the commit hashes are definately different, otherwise True</returns>
public static bool SameCommit(string commitA, string commitB) {
if (commitA == null) {
// unknown commit hash, so approve anything
return true;
}
if (commitA.Length == commitB.Length) {
return commitA == commitB;
} else if (commitA.Length >= 6 && commitB.Length >= 6) {
// depending on OS, the git log pretty output has different length (7 or 8 chars)
// if the longer hash starts with the shorter hash, return true
// Assumption: commits have different length.
var longer = commitA.Length > commitB.Length ? commitA : commitB;
var shorter = commitA.Length > commitB.Length ? commitB : commitA;
return longer.StartsWith(shorter);
}
return false;
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 39bb7d4cd9324f31b1882354b1cde762
timeCreated: 1673776105
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/BuildInfo.cs
uploadId: 752503

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d10d24dc13744197a80f50ac50f5d1a1
timeCreated: 1675449699

View File

@@ -0,0 +1,25 @@
#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(SUnityJob jobData, Type proxyJobType) {
JobPatchUtility.PatchBurstCompiledJobs(jobData, proxyJobType, unityMajorVersion:
#if UNITY_2022_2_OR_NEWER
2022
#elif UNITY_2021_3_OR_NEWER
2021
#elif UNITY_2020_3_OR_NEWER
2020
#elif UNITY_2019_4_OR_NEWER
2019
#else
2018
#endif
);
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: b9980b40e3ff447b94e71de238a37fb7
timeCreated: 1676825622
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Burst/JobHotReloadUtility.cs
uploadId: 752503

View File

@@ -0,0 +1,41 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Collections.Generic;
using System.Reflection;
namespace SingularityGroup.HotReload {
static class BurstChecker {
//Use names instead of the types directly for compat with older unity versions
const string whitelistAttrName = "BurstCompileAttribute";
const string blacklistAttrName = "BurstDiscardAttribute";
public static bool IsBurstCompiled(MethodBase method) {
//blacklist has precedence over whitelist
if(HasAttr(method.GetCustomAttributes(), blacklistAttrName)) {
return false;
}
if(HasAttr(method.GetCustomAttributes(), whitelistAttrName)) {
return true;
}
//Static methods inside a [BurstCompile] type are not burst compiled by default
if(method.DeclaringType == null || method.IsStatic) {
return false;
}
if(HasAttr(method.DeclaringType.GetCustomAttributes(), whitelistAttrName)) {
return true;
}
//No matching attributes
return false;
}
static bool HasAttr(IEnumerable<Attribute> attributes, string name) {
foreach (var attr in attributes) {
if(attr.GetType().Name == name) {
return true;
}
}
return false;
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 20dfd902e9fc4485aeef90b9add39c0a
timeCreated: 1675404225
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/BurstChecker.cs
uploadId: 752503

View File

@@ -0,0 +1,720 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using SingularityGroup.HotReload.DTO;
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;
[assembly: InternalsVisibleTo("SingularityGroup.HotReload.Editor")]
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 {
public static readonly CodePatcher I = new CodePatcher();
/// <summary>Tag for use in Debug.Log.</summary>
public const string TAG = "HotReload";
internal int PatchesApplied { get; private set; }
string PersistencePath {get;}
List<MethodPatchResponse> pendingPatches;
readonly List<MethodPatchResponse> patchHistory;
readonly HashSet<string> seenResponses = new HashSet<string>();
string[] assemblySearchPaths;
SymbolResolver symbolResolver;
readonly string tmpDir;
public FieldHandler fieldHandler;
public bool debuggerCompatibilityEnabled;
CodePatcher() {
pendingPatches = new List<MethodPatchResponse>();
patchHistory = new List<MethodPatchResponse>();
if(UnityHelper.IsEditor) {
tmpDir = PackageConst.LibraryCachePath;
} else {
tmpDir = UnityHelper.TemporaryCachePath;
}
if(!UnityHelper.IsEditor) {
PersistencePath = Path.Combine(UnityHelper.PersistentDataPath, "HotReload", "patches.json");
try {
LoadPatches(PersistencePath);
} catch(Exception ex) {
Log.Error("Encountered exception when loading patches from disk:\n{0}", ex);
}
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void InitializeUnityEvents() {
UnityEventHelper.Initialize();
}
void LoadPatches(string filePath) {
PlayerLog("Loading patches from file {0}", filePath);
var file = new FileInfo(filePath);
if(file.Exists) {
var bytes = File.ReadAllText(filePath);
var patches = JsonConvert.DeserializeObject<List<MethodPatchResponse>>(bytes);
PlayerLog("Loaded {0} patches from disk", patches.Count.ToString());
foreach (var patch in patches) {
RegisterPatches(patch, persist: false);
}
}
}
internal IReadOnlyList<MethodPatchResponse> PendingPatches => pendingPatches;
internal SymbolResolver SymbolResolver => symbolResolver;
internal string[] GetAssemblySearchPaths() {
EnsureSymbolResolver();
return assemblySearchPaths;
}
internal RegisterPatchesResult RegisterPatches(MethodPatchResponse patches, bool persist) {
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)));
pendingPatches.Add(patches);
return ApplyPatches(persist);
}
RegisterPatchesResult ApplyPatches(bool persist) {
PlayerLog("ApplyPatches. {0} patches pending.", pendingPatches.Count);
EnsureSymbolResolver();
var result = new RegisterPatchesResult();
try {
int count = 0;
foreach(var response in pendingPatches) {
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);
seenResponses.Add(response.id);
count += response.patches.Length;
}
if (count > 0) {
Dispatch.OnHotReload(result.patchedMethods).Forget();
}
} catch(Exception ex) {
Log.Warning("Exception occured when handling method patch. Exception:\n{0}", ex);
} finally {
pendingPatches.Clear();
}
if(PersistencePath != null && persist) {
SaveAppliedPatches(PersistencePath).Forget();
}
PatchesApplied++;
return result;
}
internal void ClearPatchedMethods() {
PatchesApplied = 0;
}
static bool didLog;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void WarnOnSceneLoad() {
SceneManager.sceneLoaded += (_, __) => {
if (didLog || !UnityEventHelper.UnityMethodsAdded()) {
return;
}
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.");
didLog = true;
};
}
void HandleMethodPatchResponse(MethodPatchResponse response, RegisterPatchesResult result) {
EnsureSymbolResolver();
foreach(var patch in response.patches) {
try {
foreach(var sMethod in patch.newMethods) {
var newMethod = SymbolResolver.Resolve(sMethod);
try {
UnityEventHelper.EnsureUnityEventMethod(newMethod);
} catch(Exception ex) {
Log.Warning("Encountered exception in EnsureUnityEventMethod: {0} {1}", ex.GetType().Name, ex.Message);
}
MethodUtils.DisableVisibilityChecks(newMethod);
if (!patch.patchMethods.Any(m => m.metadataToken == sMethod.metadataToken)) {
result.patchedMethods.Add(new MethodPatch(null, null, newMethod));
result.patchedSMethods.Add(sMethod);
previousPatchMethods[newMethod] = newMethod;
newMethods.Add(newMethod);
}
}
for (int i = 0; i < patch.modifiedMethods.Length; i++) {
var sOriginalMethod = patch.modifiedMethods[i];
var sPatchMethod = patch.patchMethods[i];
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));
}
}
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("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(string patchId, SMethod sOriginalMethod, SMethod sPatchMethod, bool containsBurstJobs, RegisterPatchesResult patchesResult) {
try {
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 = "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);
}
PlayerLog("Detour method {0:X8} {1}, offset: {2}", sOriginalMethod.metadataToken, patchMethod.Name, state.offset);
DetourResult result;
DetourApi.DetourMethod(state.match, patchMethod, out result);
if (result.success) {
// previous method is either original method or the last patch method
MethodBase previousMethod;
if (!previousPatchMethods.TryGetValue(state.match, out previousMethod)) {
previousMethod = state.match;
}
MethodBase originalMethod = state.match;
if (newMethods.Contains(state.match)) {
// for function added at runtime the original method should be null
originalMethod = null;
}
patchesResult.patchedMethods.Add(new MethodPatch(originalMethod, previousMethod, patchMethod));
patchesResult.patchedSMethods.Add(sOriginalMethod);
previousPatchMethods[state.match] = patchMethod;
try {
Dispatch.OnHotReloadLocal(state.match, patchMethod);
} catch {
// best effort
}
return null;
} else {
if(result.exception is InvalidProgramException && containsBurstJobs) {
//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);
}
}
struct ResolveMethodState {
public readonly SMethod originalMethod;
public readonly int offset;
public readonly bool tryLowerTokens;
public readonly bool tryHigherTokens;
public readonly MethodBase match;
public ResolveMethodState(SMethod originalMethod, int offset, bool tryLowerTokens, bool tryHigherTokens, MethodBase match) {
this.originalMethod = originalMethod;
this.offset = offset;
this.tryLowerTokens = tryLowerTokens;
this.tryHigherTokens = tryHigherTokens;
this.match = match;
}
public ResolveMethodState With(bool? tryLowerTokens = null, bool? tryHigherTokens = null, MethodBase match = null, int? offset = null) {
return new ResolveMethodState(
originalMethod,
offset ?? this.offset,
tryLowerTokens ?? this.tryLowerTokens,
tryHigherTokens ?? this.tryHigherTokens,
match ?? this.match);
}
}
struct ResolveMethodResult {
public readonly MethodBase resolvedMethod;
public readonly bool tokenOutOfRange;
public ResolveMethodResult(MethodBase resolvedMethod, bool tokenOutOfRange) {
this.resolvedMethod = resolvedMethod;
this.tokenOutOfRange = tokenOutOfRange;
}
}
ResolveMethodState TryResolveMethod(SMethod originalMethod, MethodBase patchMethod) {
var state = new ResolveMethodState(originalMethod, offset: 0, tryLowerTokens: true, tryHigherTokens: true, match: null);
var result = TryResolveMethodCore(state.originalMethod, patchMethod, 0);
if(result.resolvedMethod != null) {
return state.With(match: result.resolvedMethod);
}
state = state.With(offset: 1);
const int tries = 100000;
while(state.offset <= tries && (state.tryHigherTokens || state.tryLowerTokens)) {
if(state.tryHigherTokens) {
result = TryResolveMethodCore(originalMethod, patchMethod, state.offset);
if(result.resolvedMethod != null) {
return state.With(match: result.resolvedMethod);
} else if(result.tokenOutOfRange) {
state = state.With(tryHigherTokens: false);
}
}
if(state.tryLowerTokens) {
result = TryResolveMethodCore(originalMethod, patchMethod, -state.offset);
if(result.resolvedMethod != null) {
return state.With(match: result.resolvedMethod);
} else if(result.tokenOutOfRange) {
state = state.With(tryLowerTokens: false);
}
}
state = state.With(offset: state.offset + 1);
}
return state;
}
ResolveMethodResult TryResolveMethodCore(SMethod methodToResolve, MethodBase patchMethod, int offset) {
bool tokenOutOfRange = false;
MethodBase resolvedMethod = null;
try {
resolvedMethod = TryGetMethodBaseWithRelativeToken(methodToResolve, offset);
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) {
tokenOutOfRange = true;
} catch (ArgumentOutOfRangeException) {
tokenOutOfRange = true;
}
return new ResolveMethodResult(resolvedMethod, tokenOutOfRange);
}
MethodBase TryGetMethodBaseWithRelativeToken(SMethod sOriginalMethod, int offset) {
return symbolResolver.Resolve(new SMethod(sOriginalMethod.assemblyName,
sOriginalMethod.displayName,
sOriginalMethod.metadataToken + offset,
sOriginalMethod.simpleName));
}
string HandleMethodPatchFailure(SMethod method, Exception exception) {
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() {
if (symbolResolver == null) {
var searchPaths = new HashSet<string>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var assembliesByName = new Dictionary<string, List<Assembly>>();
for (var i = 0; i < assemblies.Length; i++) {
var name = assemblies[i].GetNameSafe();
List<Assembly> list;
if (!assembliesByName.TryGetValue(name, out list)) {
assembliesByName.Add(name, list = new List<Assembly>());
}
list.Add(assemblies[i]);
if(assemblies[i].IsDynamic) continue;
var location = assemblies[i].Location;
if(File.Exists(location)) {
searchPaths.Add(Path.GetDirectoryName(Path.GetFullPath(location)));
}
}
symbolResolver = new SymbolResolver(assembliesByName);
assemblySearchPaths = searchPaths.ToArray();
}
}
//Allow one save operation at a time.
readonly SemaphoreSlim gate = new SemaphoreSlim(1);
public async Task SaveAppliedPatches(string filePath) {
await gate.WaitAsync();
try {
await SaveAppliedPatchesNoLock(filePath);
} finally {
gate.Release();
}
}
async Task SaveAppliedPatchesNoLock(string filePath) {
if (filePath == null) {
throw new ArgumentNullException(nameof(filePath));
}
filePath = Path.GetFullPath(filePath);
var dir = Path.GetDirectoryName(filePath);
if(string.IsNullOrEmpty(dir)) {
throw new ArgumentException("Invalid path: " + filePath, nameof(filePath));
}
Directory.CreateDirectory(dir);
var history = patchHistory.ToList();
PlayerLog("Saving {0} applied patches to {1}", history.Count, filePath);
await Task.Run(() => {
using (FileStream fs = File.Create(filePath))
using (StreamWriter sw = new StreamWriter(fs))
using (JsonWriter writer = new JsonTextWriter(sw)) {
JsonSerializer serializer = JsonSerializer.Create(new JsonSerializerSettings {
Converters = new List<JsonConverter> { new MethodPatchResponsesConverter() }
});
serializer.Serialize(writer, history);
}
});
}
public void InitPatchesBlocked(string filePath) {
seenResponses.Clear();
var file = new FileInfo(filePath);
if (file.Exists) {
using(var fs = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan))
using (StreamReader sr = new StreamReader(fs))
using (JsonReader reader = new JsonTextReader(sr)) {
JsonSerializer serializer = JsonSerializer.Create(new JsonSerializerSettings {
Converters = new List<JsonConverter> { new MethodPatchResponsesConverter() }
});
pendingPatches = serializer.Deserialize<List<MethodPatchResponse>>(reader);
}
ApplyPatches(persist: false);
}
}
[StringFormatMethod("format")]
static void PlayerLog(string format, params object[] args) {
#if !UNITY_EDITOR
HotReload.Log.Info(format, args);
#endif //!UNITY_EDITOR
}
class SimpleMethodComparer : IEqualityComparer<SMethod> {
public static readonly SimpleMethodComparer I = new SimpleMethodComparer();
SimpleMethodComparer() { }
public bool Equals(SMethod x, SMethod y) => x.metadataToken == y.metadataToken;
public int GetHashCode(SMethod x) {
return x.metadataToken;
}
}
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: b6c8477b90c3f384f8124d62a5dc6e74
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/CodePatcher.cs
uploadId: 752503

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 55206f9d10104e838249bf8ac177e332
timeCreated: 1677091847

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c895e9065d763824f9211fa8054f7c2e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: ae744488364b34fcf8c80218eadc721c
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Demo/Scenes/HotReloadBasicDemo.unity
uploadId: 752503

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: fad9aa54ab3335844b5a35b9eb6ae286
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Demo/Scenes/HotReloadBurstDemo.unity
uploadId: 752503

View File

@@ -0,0 +1,66 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!850595691 &4890085278179872738
LightingSettings:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: HotReloadBurstDemoSettings
serializedVersion: 6
m_GIWorkflowMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 1
m_RealtimeEnvironmentLighting: 1
m_BounceScale: 1
m_AlbedoBoost: 1
m_IndirectOutputScale: 1
m_UsingShadowmask: 1
m_BakeBackend: 1
m_LightmapMaxSize: 1024
m_BakeResolution: 40
m_Padding: 2
m_LightmapCompression: 3
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAO: 0
m_MixedBakeMode: 2
m_LightmapsBakeMode: 1
m_FilterMode: 1
m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0}
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_RealtimeResolution: 2
m_ForceWhiteAlbedo: 0
m_ForceUpdates: 0
m_FinalGather: 0
m_FinalGatherRayCount: 256
m_FinalGatherFiltering: 1
m_PVRCulling: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVREnvironmentSampleCount: 512
m_PVREnvironmentReferencePointCount: 2048
m_LightProbeSampleCountMultiplier: 4
m_PVRBounces: 2
m_PVRMinBounces: 2
m_PVREnvironmentImportanceSampling: 0
m_PVRFilteringMode: 2
m_PVRDenoiserTypeDirect: 0
m_PVRDenoiserTypeIndirect: 0
m_PVRDenoiserTypeAO: 0
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_PVRTiledBaking: 0
m_NumRaysToShootPerTexel: -1
m_RespectSceneVisibilityWhenBakingGI: 0

View File

@@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: 961e97ae3d4011b47a1198a930f5c30d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 4890085278179872738
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Demo/Scenes/HotReloadBurstDemoSettings.lighting
uploadId: 752503

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 30c72b28fb747184ba79468d3571dea4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,217 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
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) {
openWindowButton.onClick.AddListener(Demo.I.OpenHotReloadWindow);
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()) {
informationText.text = "Hot Reload is running";
} else {
informationText.text = "Hot Reload is not running";
}
// // 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);
// // 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;
// // 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;
// }
}
// 3. Editing lambda methods
static Func<int, int> addFunction = x => {
var result = x + 10;
Debug.Log("Add: " + result);
// // uncomment to change the operator to multiply and log the result
// result = x * 10;
// Debug.Log("Multiply: " + result);
return result;
};
// 4. Editing async/await methods
async Task AsyncMethod() {
// await Task.Delay(500);
// Debug.Log("AsyncMethod");
// // silicense warning
await Task.CompletedTask;
}
// 5. Editing properties (get/set)
public static string SomeString {
// edit the get method
get {
var someStringHere = "This is some string";
return someStringHere;
}
}
// 6. Editing indexers (square bracket access such as dictionaries)
class CustomDictionary : Dictionary<string, int> {
public new int this[string key] {
get {
// // uncomment to change the indexer and log a different entry based on case
// return base[key.ToLower()];
return base[key.ToUpper()];
}
set {
base[key.ToUpper()] = value;
}
}
}
CustomDictionary randomDict = new CustomDictionary {
{ "a", 4 },
{ "A", 5 },
{ "b", 9 },
{ "B", 10 },
{ "c", 14 },
{ "C", 15 },
{ "d", 19 },
{ "D", 20 }
};
// 7. Editing operators methods (explicit and implicit operators)
public class Email {
public string Value { get; }
public Email(string value) {
Value = value;
}
// Define implicit operator
public static implicit operator string(Email value)
// 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);
// Define explicit operator
public static explicit operator Email(string value)
=> 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() {
// // 3. Editing lambda methods
// addFunction(10);
// // 4. Editing async/await methods
// AsyncMethod().Forget();
// // 5. Editing properties (get/set)
// Debug.Log(SomeString);
// // 6. Editing indexers (square bracket access such as dictionaries)
// Debug.Log(randomDict["A"]);
// // 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();
// 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");
}
// // 14. Adding event functions
// void OnDisable() {
// Debug.Log("OnDisable");
// }
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 5a2e4d3f095a9441688c70278068eee0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Demo/Scripts/HotReloadBasicDemo.cs
uploadId: 752503

View File

@@ -0,0 +1,63 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Jobs;
using UnityEngine.UI;
namespace SingularityGroup.HotReload.Demo {
public class HotReloadBurstJobsDemo : MonoBehaviour {
public Transform[] cubes;
public Text informationText;
public Button openWindowButton;
public Button openScriptButton;
public TextAsset thisScript;
TransformAccessArray cubeTransforms;
CubeJob job;
void Awake() {
cubeTransforms = new TransformAccessArray(cubes);
if(Application.isEditor) {
openWindowButton.onClick.AddListener(Demo.I.OpenHotReloadWindow);
openScriptButton.onClick.AddListener(() => Demo.I.OpenScriptFile(thisScript, 49, 17));
} else {
openWindowButton.gameObject.SetActive(false);
openScriptButton.gameObject.SetActive(false);
}
informationText.gameObject.SetActive(true);
}
void Update() {
job.deltaTime = Time.deltaTime;
job.time = Time.time;
var handle = job.Schedule(cubeTransforms);
handle.Complete();
if (Demo.I.IsServerRunning()) {
informationText.text = "Hot Reload is running";
} else {
informationText.text = "Hot Reload is not running";
}
}
struct CubeJob : IJobParallelForTransform {
public float deltaTime;
public float time;
public void Execute(int index, TransformAccess transform) {
transform.localRotation *= Quaternion.Euler(50 * deltaTime, 0, 0);
// Uncomment this code to scale the cubes
// var scale = Mathf.Abs(Mathf.Sin(time));
// transform.localScale = new Vector3(scale, scale, scale);
// Uncomment this code to make the cube move from left to right and back
// transform.position += (transform.localScale.x < 0.5 ? Vector3.left : Vector3.right) * deltaTime;
}
}
void OnDestroy() {
cubeTransforms.Dispose();
}
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: e09948cf1f317d04fbaf410dbfe91656
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Demo/Scripts/HotReloadBurstJobsDemo.cs
uploadId: 752503

View File

@@ -0,0 +1,29 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using UnityEngine;
namespace SingularityGroup.HotReload.Demo {
public interface IDemo {
bool IsServerRunning();
void OpenHotReloadWindow();
void OpenScriptFile(TextAsset textAsset, int line, int column);
}
public static class Demo {
public static IDemo I = new PlayerDemo();
}
public class PlayerDemo : IDemo {
public bool IsServerRunning() {
return ServerHealthCheck.I.IsServerHealthy;
}
public void OpenHotReloadWindow() {
//no-op
}
public void OpenScriptFile(TextAsset textAsset, int line, int column) {
//no-op
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 04dccdcced0245f1830021fdcad1d28a
timeCreated: 1677321944
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Demo/Scripts/IDemo.cs
uploadId: 752503

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 0dc8d7047b14c44b7970c5d35665dbe1
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/HotReloadPrompts.prefab
uploadId: 752503

View File

@@ -0,0 +1,140 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Linq;
using JetBrains.Annotations;
using System.IO;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SingularityGroup.HotReload {
/// <summary>
/// HotReload runtime settings. These can be changed while the app is running.
/// </summary>
/// <remarks>
/// ScriptableObject that may be included in Resources/ folder.
/// See also Editor/PrebuildIncludeResources.cs
/// </remarks>
[Serializable]
class HotReloadSettingsObject : ScriptableObject {
#region singleton
private static HotReloadSettingsObject _I;
public static HotReloadSettingsObject I {
get {
if (_I == null) {
_I = LoadSettingsOrDefault();
}
return _I;
}
}
/// <summary>Create settings inside Assets/ because user cannot edit files that are included inside a Unity package</summary>
/// <remarks>
/// You can change this in a build script if you want it created somewhere else.
/// </remarks>
public static string editorAssetPath = "Assets/HotReload/Resources/HotReloadSettingsObject.asset";
private static string resourceName => Path.GetFileNameWithoutExtension(editorAssetPath);
public static bool TryLoadSettings(out HotReloadSettingsObject settings) {
try {
settings = LoadSettings();
return settings != null;
} catch(FileNotFoundException) {
settings = null;
return false;
}
}
[NotNull]
private static HotReloadSettingsObject LoadSettingsOrDefault() {
var settings = LoadSettings();
if (settings == null) {
// load defaults
settings = CreateInstance<HotReloadSettingsObject>();
}
return settings;
}
[CanBeNull]
private static HotReloadSettingsObject LoadSettings() {
HotReloadSettingsObject settings;
if (Application.isEditor) {
#if UNITY_EDITOR
settings = AssetDatabase.LoadAssetAtPath<HotReloadSettingsObject>(editorAssetPath);
#else
settings = null;
#endif
} else {
// load from Resources (assumes that build includes the resource)
settings = Resources.Load<HotReloadSettingsObject>(resourceName);
}
return settings;
}
#endregion
#region settings
/// <summary>Set default values.</summary>
/// <remarks>
/// This is called by the Unity editor when the ScriptableObject is first created.
/// This function is only called in editor mode.
/// </remarks>
private void Reset() {
EnsurePrefabSetCorrectly();
}
/// <summary>
/// Path to the prefab asset file.
/// </summary>
const string prefabAssetPath = "Packages/com.singularitygroup.hotreload/Runtime/HotReloadPrompts.prefab";
// Call this during build, just to be sure the field is correct. (I had some issues with it while editing the prefab)
public void EnsurePrefabSetCorrectly() {
#if UNITY_EDITOR
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabAssetPath);
if (prefab == null) {
// when you use HotReload as a unitypackage, prefab is somewhere inside your assets folder
var guids = AssetDatabase.FindAssets("HotReloadPrompts t:prefab", new string[]{"Assets"});
var paths = guids.Select(guid => AssetDatabase.GUIDToAssetPath(guid));
var promptsPrefabPath = paths.FirstOrDefault(assetpath => Path.GetFileName(assetpath) == "HotReloadPrompts.prefab");
if (promptsPrefabPath != null) {
prefab = AssetDatabase.LoadAssetAtPath<GameObject>(promptsPrefabPath);
}
}
if (prefab == null) {
throw new Exception("Failed to find PromptsPrefab (are you using Hot Reload as a package?");
}
PromptsPrefab = prefab;
#endif
}
public void EnsurePrefabNotInBuild() {
#if UNITY_EDITOR
PromptsPrefab = null;
#endif
}
// put the stored settings here
[Header("Build Settings")]
[Tooltip("Should the Hot Reload runtime be included in development builds? HotReload is never included in release builds.")]
public bool IncludeInBuild = true;
[Header("Player Settings")]
public bool AllowAndroidAppToMakeHttpRequests = false;
#region hidden
/// Reference to the Prefab, for loading it at runtime
[HideInInspector]
public GameObject PromptsPrefab;
#endregion
#endregion settings
}
}
#endif

View File

@@ -0,0 +1,20 @@
fileFormatVersion: 2
guid: 324c6fd3c103e0f418eb4b98c46bf63c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- PromptsPrefab: {fileID: 4967086677379066170, guid: 0dc8d7047b14c44b7970c5d35665dbe1,
type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/HotReloadSettingsObject.cs
uploadId: 752503

View File

@@ -0,0 +1,19 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System.Net.Http;
namespace SingularityGroup.HotReload {
public class HttpClientUtils {
public static HttpClient CreateHttpClient() {
var handler = new HttpClientHandler {
// Without this flag HttpClients don't work for PCs with double-byte characters in the name
UseCookies = false
};
return new HttpClient(handler);
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: b40f5d8cac104565b0aaa1d1e294ff8f
timeCreated: 1700069330
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/HttpClientUtils.cs
uploadId: 752503

View File

@@ -0,0 +1,11 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
namespace SingularityGroup.HotReload {
public interface IServerHealthCheck {
bool IsServerHealthy { get; }
}
internal interface IServerHealthCheckInternal : IServerHealthCheck {
void CheckHealth();
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: bcb0ff221290427182643b815685ea97
timeCreated: 1675232020
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/IServerHealthCheck.cs
uploadId: 752503

View File

@@ -0,0 +1,27 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using UnityEngine;
using UnityEngine.UI;
namespace SingularityGroup.HotReload {
class InstallQRDialog : MonoBehaviour {
public Button buttonGo;
public Button buttonHide;
private void Start() {
buttonHide.onClick.AddListener(Hide);
// launch camera app that can scan QR-Code https://singularitygroup.atlassian.net/browse/SG-29495
buttonGo.onClick.AddListener(() => {
Hide();
var recommendedQrCodeApp = "com.scanteam.qrcodereader";
Application.OpenURL($"https://play.google.com/store/apps/details?id={recommendedQrCodeApp}");
});
}
/// hide this dialog
void Hide() {
gameObject.SetActive(false); // this should disable the Update loop?
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 03d3be3b485a4450b112f9ea3af4fb66
timeCreated: 1674988075
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/InstallQRDialog.cs
uploadId: 752503

View File

@@ -0,0 +1,64 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
#if UNITY_ANDROID && !UNITY_EDITOR
#define MOBILE_ANDROID
#endif
#if UNITY_IOS && !UNITY_EDITOR
#define MOBILE_IOS
#endif
#if MOBILE_ANDROID || MOBILE_IOS
#define MOBILE
#endif
using System;
using System.Net.NetworkInformation;
using System.Net.Sockets;
namespace SingularityGroup.HotReload {
static class IpHelper {
// get my local ip address
static DateTime cachedAt;
static string ipCached;
public static string GetIpAddressCached() {
if (string.IsNullOrEmpty(ipCached) || DateTime.UtcNow - cachedAt > TimeSpan.FromSeconds(5)) {
ipCached = GetIpAddress();
cachedAt = DateTime.UtcNow;
}
return ipCached;
}
public static string GetIpAddress() {
var ip = GetLocalIPv4(NetworkInterfaceType.Wireless80211);
if (string.IsNullOrEmpty(ip)) {
return GetLocalIPv4(NetworkInterfaceType.Ethernet);
}
return ip;
}
private static string GetLocalIPv4(NetworkInterfaceType _type) {
string output = "";
foreach (NetworkInterface item in NetworkInterface.GetAllNetworkInterfaces()) {
if (item.NetworkInterfaceType == _type && item.OperationalStatus == OperationalStatus.Up) {
foreach (UnicastIPAddressInformation ip in item.GetIPProperties().UnicastAddresses) {
if (ip.Address.AddressFamily == AddressFamily.InterNetwork && IsLocalIp(ip.Address.MapToIPv4().GetAddressBytes())) {
output = ip.Address.ToString();
}
}
}
}
return output;
}
// https://datatracker.ietf.org/doc/html/rfc1918#section-3
static bool IsLocalIp(byte[] ipAddress) {
return ipAddress[0] == 10
|| ipAddress[0] == 172
&& ipAddress[1] >= 16
&& ipAddress[1] <= 31
|| ipAddress[0] == 192
&& ipAddress[1] == 168;
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 4d3a24a25ced4eae8b7e0b9b5a0d5c9d
timeCreated: 1674145172
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/IpHelper.cs
uploadId: 752503

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 053fc5684eb47f54e8c877cb1ade54d6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 520640393141aab41bd6d6b1f43e7037
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,99 @@
fileFormatVersion: 2
guid: e0277ee5c436c344a9d7720bdc0391d1
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 1
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude WebGL: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
- first:
WebGL: WebGL
second:
enabled: 0
settings: {}
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Libs/EditorOnly/SingularityGroup.HotReload.RuntimeDependencies.dll
uploadId: 752503

View File

@@ -0,0 +1,95 @@
fileFormatVersion: 2
guid: 49b66a954ad81dd4795e880bd63dc4c3
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints:
- UNITY_2019_4_OR_NEWER
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 1
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude WebGL: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Libs/EditorOnly/SingularityGroup.HotReload.RuntimeDependencies2019.dll
uploadId: 752503

View File

@@ -0,0 +1,95 @@
fileFormatVersion: 2
guid: 784812c918589424a90509ea34a51da0
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints:
- UNITY_2020_3_OR_NEWER
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 1
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude WebGL: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Libs/EditorOnly/SingularityGroup.HotReload.RuntimeDependencies2020.dll
uploadId: 752503

View File

@@ -0,0 +1,95 @@
fileFormatVersion: 2
guid: 8c8658e0b34ecf04ca6ca07ffa6fc846
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints:
- UNITY_2022_2_OR_NEWER
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 1
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude WebGL: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Libs/EditorOnly/SingularityGroup.HotReload.RuntimeDependencies2022.dll
uploadId: 752503

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b3d6360d6d1f2c47b659f0a4960ebfe
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,95 @@
fileFormatVersion: 2
guid: 15528e9db0a6c9b45a66378f0b6c4dd6
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints:
- ENABLE_MONO
- DEVELOPMENT_BUILD
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 1
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 0
Exclude Editor: 1
Exclude Linux64: 0
Exclude OSXUniversal: 0
Exclude Win: 0
Exclude Win64: 0
Exclude iOS: 0
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 1
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 1
settings:
CPU: x86_64
- first:
Standalone: Win
second:
enabled: 1
settings:
CPU: x86
- first:
Standalone: Win64
second:
enabled: 1
settings:
CPU: x86_64
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 1
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Libs/OnDevice/SingularityGroup.HotReload.RuntimeDependencies.dll
uploadId: 752503

View File

@@ -0,0 +1,96 @@
fileFormatVersion: 2
guid: 4febf8334e6a82f4e9faf3513c7fcc8d
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints:
- ENABLE_MONO
- UNITY_2019_4_OR_NEWER
- DEVELOPMENT_BUILD
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 1
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 0
Exclude Editor: 1
Exclude Linux64: 0
Exclude OSXUniversal: 0
Exclude Win: 0
Exclude Win64: 0
Exclude iOS: 0
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 1
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 1
settings:
CPU: x86_64
- first:
Standalone: Win
second:
enabled: 1
settings:
CPU: x86
- first:
Standalone: Win64
second:
enabled: 1
settings:
CPU: x86_64
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 1
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Libs/OnDevice/SingularityGroup.HotReload.RuntimeDependencies2019.dll
uploadId: 752503

View File

@@ -0,0 +1,96 @@
fileFormatVersion: 2
guid: 35c3cec01c5230e41bea51d3ac6fcfa1
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints:
- ENABLE_MONO
- UNITY_2020_3_OR_NEWER
- DEVELOPMENT_BUILD
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 1
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 0
Exclude Editor: 1
Exclude Linux64: 0
Exclude OSXUniversal: 0
Exclude Win: 0
Exclude Win64: 0
Exclude iOS: 0
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 1
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 1
settings:
CPU: x86_64
- first:
Standalone: Win
second:
enabled: 1
settings:
CPU: x86
- first:
Standalone: Win64
second:
enabled: 1
settings:
CPU: x86_64
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 1
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Libs/OnDevice/SingularityGroup.HotReload.RuntimeDependencies2020.dll
uploadId: 752503

View File

@@ -0,0 +1,96 @@
fileFormatVersion: 2
guid: c9f10603236554c4896f310072d57f24
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints:
- ENABLE_MONO
- UNITY_2022_2_OR_NEWER
- DEVELOPMENT_BUILD
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 1
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 0
Exclude Editor: 1
Exclude Linux64: 0
Exclude OSXUniversal: 0
Exclude Win: 0
Exclude Win64: 0
Exclude iOS: 0
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 1
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 1
settings:
CPU: x86_64
- first:
Standalone: Win
second:
enabled: 1
settings:
CPU: x86
- first:
Standalone: Win64
second:
enabled: 1
settings:
CPU: x86_64
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 1
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Libs/OnDevice/SingularityGroup.HotReload.RuntimeDependencies2022.dll
uploadId: 752503

View File

@@ -0,0 +1,116 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Reflection;
using SingularityGroup.HotReload.MonoMod.Utils;
namespace SingularityGroup.HotReload {
static class MethodCompatiblity {
internal static string CheckCompatibility(MethodBase previousMethod, MethodBase patchMethod) {
var previousConstructor = previousMethod as ConstructorInfo;
var patchConstructor = patchMethod as ConstructorInfo;
if(previousConstructor != null && !ReferenceEquals(patchConstructor, null)) {
return AreConstructorsCompatible(previousConstructor, patchConstructor);
}
var previousMethodInfo = previousMethod as MethodInfo;
var patchMethodInfo = patchMethod as MethodInfo;
if(!ReferenceEquals(previousMethodInfo, null) && !ReferenceEquals(patchMethodInfo, null)) {
return AreMethodInfosCompatible(previousMethodInfo, patchMethodInfo);
}
return "unknown issue";
}
static string AreMethodBasesCompatible(MethodBase previousMethod, MethodBase patchMethod) {
if(previousMethod.Name != patchMethod.Name) {
return "Method name mismatch";
}
//Declaring type of patch method is different from the target method but their full name (namespace + name) is equal
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 "IsGenericMethodDefinition mismatch";
}
var prevParams = previousMethod.GetParameters();
var patchParams = patchMethod.GetParameters();
ArraySegment<ParameterInfo> patchParamsSegment;
bool patchMethodHasExplicitThis;
if(previousMethod.IsStatic || previousMethod.Name.Contains("<") && !patchMethod.IsStatic) {
patchMethodHasExplicitThis = false;
} else {
patchMethodHasExplicitThis = true;
}
if(LikelyHasExplicitThis(prevParams, patchParams, previousMethod)) {
patchMethodHasExplicitThis = true;
}
//Special edge case: User added static keyword to method. No explicit this will be generated in that case
if(!previousMethod.IsStatic && patchMethod.IsStatic && !LikelyHasExplicitThis(prevParams, patchParams, previousMethod)) {
patchMethodHasExplicitThis = false;
}
if(patchMethodHasExplicitThis) {
//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 "missing this parameter";
}
//this parameter has to be the declaring type
if(!ParamTypeMatches(patchParams[0].ParameterType, previousMethod.DeclaringType)) {
return "this parameter type mismatch";
}
//Ignore the this parameter and compare the remaining ones.
patchParamsSegment = new ArraySegment<ParameterInfo>(patchParams, 1, patchParams.Length - 1);
} else {
patchParamsSegment = new ArraySegment<ParameterInfo>(patchParams);
}
return CompareParameters(new ArraySegment<ParameterInfo>(prevParams), patchParamsSegment);
}
static bool LikelyHasExplicitThis(ParameterInfo[] prevParams, ParameterInfo[] patchParams, MethodBase previousMethod) {
if (patchParams.Length != prevParams.Length + 1) {
return false;
}
var patchT = patchParams[0].ParameterType;
if (!ParamTypeMatches(patchT, previousMethod.DeclaringType)) {
return false;
}
return patchParams[0].Name == "this";
}
static bool ParamTypeMatches(Type patchT, Type originalT) {
return patchT == originalT || patchT.IsByRef && patchT.GetElementType() == originalT;
}
static string CompareParameters(ArraySegment<ParameterInfo> x, ArraySegment<ParameterInfo> y) {
if(x.Count != y.Count) {
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 "parameter type mismatch";
}
}
return null;
}
static string AreConstructorsCompatible(ConstructorInfo x, ConstructorInfo y) {
return AreMethodBasesCompatible(x, y);
}
static string AreMethodInfosCompatible(MethodInfo x, MethodInfo y) {
return AreMethodBasesCompatible(x, y) ?? (x.ReturnType == y.ReturnType ? null : "Return type mismatch");
}
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: d731e763662b98941bb06ffc6994a9a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/MethodCompatiblity.cs
uploadId: 752503

View File

@@ -0,0 +1,716 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Collections.Generic;
using SingularityGroup.HotReload.DTO;
using SingularityGroup.HotReload.Newtonsoft.Json;
namespace SingularityGroup.HotReload.JsonConverters {
internal class MethodPatchResponsesConverter : JsonConverter {
public override bool CanConvert(Type objectType) {
return objectType == typeof(List<MethodPatchResponse>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
var list = new List<MethodPatchResponse>();
while (reader.Read()) {
if (reader.TokenType == JsonToken.StartObject) {
list.Add(ReadMethodPatchResponse(reader));
} else if (reader.TokenType == JsonToken.EndArray) {
break; // End of the SMethod list
}
}
return list;
}
private MethodPatchResponse ReadMethodPatchResponse(JsonReader reader) {
string id = null;
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) {
break;
}
if (reader.TokenType != JsonToken.PropertyName) {
continue;
}
var propertyName = (string)reader.Value;
switch (propertyName) {
case nameof(MethodPatchResponse.id):
id = reader.ReadAsString();
break;
case nameof(MethodPatchResponse.patches):
patches = ReadPatches(reader);
break;
case nameof(MethodPatchResponse.failures):
failures = ReadStringArray(reader);
break;
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
break;
}
}
return new MethodPatchResponse(
id ?? string.Empty,
patches ?? Array.Empty<CodePatch>(),
failures ?? Array.Empty<string>(),
removedMethod ?? Array.Empty<SMethod>(),
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>()
);
}
private CodePatch[] ReadPatches(JsonReader reader) {
var patches = new List<CodePatch>();
while (reader.Read()) {
if (reader.TokenType == JsonToken.EndArray) {
break;
}
if (reader.TokenType != JsonToken.StartObject) {
continue;
}
string patchId = null;
string assemblyName = null;
byte[] patchAssembly = null;
byte[] patchPdb = null;
SMethod[] modifiedMethods = null;
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) {
break;
}
if (reader.TokenType != JsonToken.PropertyName) {
continue;
}
var propertyName = (string)reader.Value;
switch (propertyName) {
case nameof(CodePatch.patchId):
patchId = reader.ReadAsString();
break;
case nameof(CodePatch.assemblyName):
assemblyName = reader.ReadAsString();
break;
case nameof(CodePatch.patchAssembly):
patchAssembly = Convert.FromBase64String(reader.ReadAsString());
break;
case nameof(CodePatch.patchPdb):
patchPdb = Convert.FromBase64String(reader.ReadAsString());
break;
case nameof(CodePatch.modifiedMethods):
modifiedMethods = ReadSMethodArray(reader);
break;
case nameof(CodePatch.patchMethods):
patchMethods = ReadSMethodArray(reader);
break;
case nameof(CodePatch.newMethods):
newMethods = ReadSMethodArray(reader);
break;
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
break;
}
}
patches.Add(new CodePatch(
patchId: patchId ?? string.Empty,
assemblyName: assemblyName ?? string.Empty,
patchAssembly: patchAssembly ?? Array.Empty<byte>(),
patchPdb: patchPdb ?? Array.Empty<byte>(),
modifiedMethods: modifiedMethods ?? Array.Empty<SMethod>(),
patchMethods: patchMethods ?? Array.Empty<SMethod>(),
newMethods: newMethods ?? Array.Empty<SMethod>(),
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>()
));
}
return patches.ToArray();
}
private string[] ReadStringArray(JsonReader reader) {
var list = new List<string>();
while (reader.Read()) {
if (reader.TokenType == JsonToken.String) {
list.Add((string)reader.Value);
} else if (reader.TokenType == JsonToken.EndArray) {
break; // End of the string list
}
}
return list.ToArray();
}
private SMethod[] ReadSMethodArray(JsonReader reader) {
var list = new List<SMethod>();
while (reader.Read()) {
if (reader.TokenType == JsonToken.StartObject) {
list.Add(ReadSMethod(reader));
} else if (reader.TokenType == JsonToken.EndArray) {
break; // End of the SMethod list
}
}
return list.ToArray();
}
private SType[] ReadSTypeArray(JsonReader reader) {
var list = new List<SType>();
while (reader.Read()) {
if (reader.TokenType == JsonToken.StartObject) {
list.Add(ReadSType(reader));
} else if (reader.TokenType == JsonToken.EndArray) {
break; // End of the SType list
}
}
return list.ToArray();
}
private SUnityJob[] ReadSUnityJobArray(JsonReader reader) {
var array = new List<SUnityJob>();
while (reader.Read()) {
if (reader.TokenType == JsonToken.StartObject) {
array.Add(ReadSUnityJob(reader));
} else if (reader.TokenType == JsonToken.EndArray) {
break; // End of the SUnityJob array
}
}
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);
string simpleName = 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(SMethod.assemblyName):
assemblyName = reader.ReadAsString();
break;
case nameof(SMethod.displayName):
displayName = reader.ReadAsString();
break;
case nameof(SMethod.metadataToken):
metadataToken = reader.ReadAsInt32() ?? default(int);
break;
case nameof(SMethod.simpleName):
simpleName = reader.ReadAsString();
break;
default:
reader.Skip(); // Skip unknown properties
break;
}
}
return new SMethod(
assemblyName ?? string.Empty,
displayName ?? string.Empty,
metadataToken,
simpleName ?? string.Empty
);
}
private SType ReadSType(JsonReader reader) {
string assemblyName = null;
string typeName = null;
int? metadataToken = null;
while (reader.Read()) {
if (reader.TokenType == JsonToken.Null) {
return null;
}
if (reader.TokenType == JsonToken.EndObject) {
break;
}
if (reader.TokenType != JsonToken.PropertyName) {
continue;
}
var propertyName = (string)reader.Value;
switch (propertyName) {
case nameof(SType.assemblyName):
assemblyName = reader.ReadAsString();
break;
case nameof(SType.typeName):
typeName = reader.ReadAsString();
break;
case nameof(SType.metadataToken):
metadataToken = reader.ReadAsInt32();
break;
default:
reader.Skip(); // Skip unknown properties
break;
}
}
return new SType(
assemblyName ?? string.Empty,
typeName ?? string.Empty,
metadataToken ?? 0
);
}
private SUnityJob ReadSUnityJob(JsonReader reader) {
int metadataToken = default(int);
UnityJobKind jobKind = default(UnityJobKind);
while (reader.Read()) {
if (reader.TokenType == JsonToken.EndObject) {
break;
}
if (reader.TokenType != JsonToken.PropertyName) {
continue;
}
var propertyName = (string)reader.Value;
switch (propertyName) {
case nameof(SUnityJob.metadataToken):
metadataToken = reader.ReadAsInt32() ?? 0;
break;
case nameof(SUnityJob.jobKind):
var jobKindStr = reader.ReadAsString();
Enum.TryParse(jobKindStr, out jobKind);
break;
default:
reader.Skip(); // Skip unknown properties
break;
}
}
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;
if (responses == null) {
writer.WriteNull();
return;
}
writer.WriteStartArray();
foreach (var response in responses) {
writer.WriteStartObject();
writer.WritePropertyName(nameof(response.id));
writer.WriteValue(response.id);
if (response.patches != null) {
writer.WritePropertyName(nameof(response.patches));
writer.WriteStartArray();
foreach (var responsePatch in response.patches) {
writer.WriteStartObject();
writer.WritePropertyName(nameof(responsePatch.patchId));
writer.WriteValue(responsePatch.patchId);
writer.WritePropertyName(nameof(responsePatch.assemblyName));
writer.WriteValue(responsePatch.assemblyName);
writer.WritePropertyName(nameof(responsePatch.patchAssembly));
writer.WriteValue(Convert.ToBase64String(responsePatch.patchAssembly));
writer.WritePropertyName(nameof(responsePatch.patchPdb));
writer.WriteValue(Convert.ToBase64String(responsePatch.patchPdb));
if (responsePatch.modifiedMethods != null) {
writer.WritePropertyName(nameof(responsePatch.modifiedMethods));
writer.WriteStartArray();
foreach (var modifiedMethod in responsePatch.modifiedMethods) {
WriteSMethod(writer, modifiedMethod);
}
writer.WriteEndArray();
}
if (responsePatch.patchMethods != null) {
writer.WritePropertyName(nameof(responsePatch.patchMethods));
writer.WriteStartArray();
foreach (var patchMethod in responsePatch.patchMethods) {
WriteSMethod(writer, patchMethod);
}
writer.WriteEndArray();
}
if (responsePatch.newMethods != null) {
writer.WritePropertyName(nameof(responsePatch.newMethods));
writer.WriteStartArray();
foreach (var newMethod in responsePatch.newMethods) {
WriteSMethod(writer, newMethod);
}
writer.WriteEndArray();
}
if (responsePatch.unityJobs != null) {
writer.WritePropertyName(nameof(responsePatch.unityJobs));
writer.WriteStartArray();
foreach (var unityJob in responsePatch.unityJobs) {
writer.WriteStartObject();
writer.WritePropertyName(nameof(unityJob.metadataToken));
writer.WriteValue(unityJob.metadataToken);
writer.WritePropertyName(nameof(unityJob.jobKind));
writer.WriteValue(unityJob.jobKind.ToString());
writer.WriteEndObject();
}
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();
}
writer.WriteEndArray();
}
if (response.failures != null) {
writer.WritePropertyName(nameof(response.failures));
writer.WriteStartArray();
foreach (var failure in response.failures) {
writer.WriteValue(failure);
}
writer.WriteEndArray();
}
if (response.removedMethod != null) {
writer.WritePropertyName(nameof(response.removedMethod));
writer.WriteStartArray();
foreach (var removedMethod in response.removedMethod) {
WriteSMethod(writer, removedMethod);
}
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();
}
void WriteSMethod(JsonWriter writer, SMethod method) {
writer.WriteStartObject();
writer.WritePropertyName(nameof(method.assemblyName));
writer.WriteValue(method.assemblyName);
writer.WritePropertyName(nameof(method.displayName));
writer.WriteValue(method.displayName);
writer.WritePropertyName(nameof(method.metadataToken));
writer.WriteValue(method.metadataToken);
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();
}
}
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);
writer.WritePropertyName(nameof(type.metadataToken));
writer.WriteValue(type.metadataToken);
writer.WriteEndObject();
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: a35195732a094716a76d7125122c90fe
timeCreated: 1685732397
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/MethodPatchResponsesConverter.cs
uploadId: 752503

View File

@@ -0,0 +1,35 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Reflection;
namespace SingularityGroup.HotReload {
static class MethodUtils {
#if ENABLE_MONO
public static unsafe void DisableVisibilityChecks(MethodBase method) {
if(IntPtr.Size == sizeof(long)) {
var ptr = (Interop.MonoMethod64*)method.MethodHandle.Value.ToPointer();
ptr->monoMethodFlags |= Interop.MonoMethodFlags.skip_visibility;
} else {
var ptr = (Interop.MonoMethod32*)method.MethodHandle.Value.ToPointer();
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
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c4f19b17adc17a94192a325012f153db
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/MethodUtils.cs
uploadId: 752503

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a2fd781ae18045f0b7690cd490737996
timeCreated: 1675064423

View File

@@ -0,0 +1,80 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using UnityEngine;
using UnityEngine.UI;
namespace SingularityGroup.HotReload {
internal class ConnectionDialog : MonoBehaviour {
[Header("UI controls")]
public Button buttonHide;
[Header("Information")]
public Text textSummary;
public Text textSuggestion;
void Start() {
buttonHide.onClick.AddListener(Hide);
}
public int pendingPatches = 0;
public int patchesApplied = 0;
private void Awake() {
SyncPatchCounts();
}
bool SyncPatchCounts() {
var changed = false;
if (pendingPatches != CodePatcher.I.PendingPatches.Count) {
pendingPatches = CodePatcher.I.PendingPatches.Count;
changed = true;
}
if (patchesApplied != CodePatcher.I.PatchesApplied) {
patchesApplied = CodePatcher.I.PatchesApplied;
changed = true;
}
return changed;
}
/// <param name="summary">One of the <see cref="ConnectionSummary"/> constants</param>
public void SetSummary(string summary = ConnectionSummary.Connected) {
if (textSummary != null) textSummary.text = summary;
isConnected = summary == ConnectionSummary.Connected;
}
private bool isConnected = false;
// assumes that auto-pair already tried for several seconds
void Update() {
textSuggestion.enabled = isConnected;
if (SyncPatchCounts()) {
textSuggestion.text = $"Patches: {pendingPatches} pending, {patchesApplied} applied";
}
}
/// hide this dialog
void Hide() {
gameObject.SetActive(false); // this should disable the Update loop?
}
}
/// <summary>
/// The connection between device and Hot Reload can be summarized in a few words.
/// </summary>
/// <remarks>
/// The summary may be shown for less than a second, as the connection can change without warning.<br/>
/// Therefore, we use short and simple messages.
/// </remarks>
internal static class ConnectionSummary {
public const string Cancelled = "Cancelled";
public const string Connecting = "Connecting ...";
public const string Handshaking = "Handshaking ...";
public const string DifferencesFound = "Differences found";
public const string Connected = "Connected!";
// reconnecting can be shown for a long time, so a longer message is okay
public const string TryingToReconnect = "Trying to reconnect ...";
public const string Disconnected = "Disconnected";
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: bb1cc47c374f478e861f2c3dade07e1a
timeCreated: 1675064498
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/MonoBehaviours/ConnectionDialog.cs
uploadId: 752503

View File

@@ -0,0 +1,145 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
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 {
public GameObject retryPrompt;
public GameObject connectedPrompt;
public GameObject questionPrompt;
[Header("Other")]
[Tooltip("Used when project does not create an EventSystem early enough")]
public GameObject fallbackEventSystem;
#region Singleton
private static Prompts _I;
/// <summary>
/// All usages must check that <see cref="PlayerEntrypoint.RuntimeSupportsHotReload"/> is true before accessing this singleton.
/// </summary>
/// <remarks>
/// This getter can throw on unsupported platforms (HotReloadSettingsObject resource doesn't exist on unsupported platforms).
/// </remarks>
public static Prompts I {
get {
if (_I == null) {
// allow showing prompts in editor (for testing)
if (!Application.isEditor && !PlayerEntrypoint.IsPlayerWithHotReload()) {
throw new NotSupportedException("IsPlayerWithHotReload() is false");
}
var go = Instantiate(HotReloadSettingsObject.I.PromptsPrefab,
new Vector3(0, 0, 0), Quaternion.identity);
go.name = nameof(Prompts) + "_singleton";
if (Application.isPlaying) {
DontDestroyOnLoad(go);
}
_I = go.GetComponentInChildren<Prompts>();
}
return _I;
}
}
#endregion
/// <seealso cref="ShowConnectionDialog"/>
public static void SetConnectionState(string state, bool log = true) {
var connectionDialog = I.connectedPrompt.GetComponentInChildren<ConnectionDialog>();
if (log) Log.Debug($"SetConnectionState( {state} )");
if (connectionDialog) {
connectionDialog.SetSummary(state);
}
}
/// <seealso cref="SetConnectionState"/>
public static void ShowConnectionDialog() {
I.retryPrompt.SetActive(false);
I.connectedPrompt.SetActive(true);
}
public static async Task<bool> ShowQuestionDialog(QuestionDialog.Config config) {
var tcs = new TaskCompletionSource<bool>();
var holder = I.questionPrompt;
var dialog = holder.GetComponentInChildren<QuestionDialog>();
dialog.completion = tcs;
dialog.UpdateView(config);
holder.SetActive(true);
return await tcs.Task;
}
public static void ShowRetryDialog(
PatchServerInfo patchServerInfo,
ServerHandshake.Result handshakeResults = ServerHandshake.Result.None,
bool auto = true
) {
var retryDialog = I.retryPrompt.GetComponentInChildren<RetryDialog>();
RetryDialog.TargetServer = patchServerInfo;
RetryDialog.HandshakeResults = handshakeResults;
if (patchServerInfo == null) {
retryDialog.DebugInfo = $"patchServerInfo == null {handshakeResults}";
} else {
retryDialog.DebugInfo = $"{RequestHelper.CreateUrl(patchServerInfo)} {handshakeResults}";
}
retryDialog.autoConnect = auto;
I.connectedPrompt.SetActive(false);
I.retryPrompt.SetActive(true);
}
#region fallback event system
private void Start() {
StartCoroutine(DelayedEnsureEventSystem());
}
private bool userTriedToInteract = false;
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
}
}
private IEnumerator DelayedEnsureEventSystem() {
// allow some delay in-case the project loads the EventSystem asynchronously (perhaps in a second scene)
if (EventSystem.current == null) {
yield return new WaitForSeconds(1f);
DoEnsureEventSystem();
}
}
/// Scene must contain an EventSystem and StandaloneInputModule, otherwise clicking/tapping on the overlay does nothing.
private void DoEnsureEventSystem() {
if (EventSystem.current == null) {
Log.Info($"No EventSystem is active, enabling an EventSystem inside Hot Reload {name} prefab." +
" A Unity EventSystem and an Input module is required for tapping buttons on the Unity UI.");
fallbackEventSystem.SetActive(true);
}
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d92cdbfacafd433ca77184c22a384a6d
timeCreated: 1674488132
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/MonoBehaviours/Prompts.cs
uploadId: 752503

View File

@@ -0,0 +1,64 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
namespace SingularityGroup.HotReload {
class QuestionDialog : MonoBehaviour {
[Header("Information")]
public Text textSummary;
public Text textSuggestion;
[Header("UI controls")]
public Button buttonContinue;
public Button buttonCancel;
public Button buttonMoreInfo;
public TaskCompletionSource<bool> completion = new TaskCompletionSource<bool>();
public void UpdateView(Config config) {
textSummary.text = config.summary;
textSuggestion.text = config.suggestion;
if (string.IsNullOrEmpty(config.continueButtonText)) {
buttonContinue.enabled = false;
} else {
buttonContinue.GetComponentInChildren<Text>().text = config.continueButtonText;
buttonContinue.onClick.AddListener(() => {
completion.TrySetResult(true);
Hide();
});
}
if (string.IsNullOrEmpty(config.cancelButtonText)) {
buttonCancel.enabled = false;
} else {
buttonCancel.GetComponentInChildren<Text>().text = config.cancelButtonText;
buttonCancel.onClick.AddListener(() => {
completion.TrySetResult(false);
Hide();
});
}
buttonMoreInfo.onClick.AddListener(() => {
Application.OpenURL(config.moreInfoUrl);
});
}
internal class Config {
public string summary;
public string suggestion;
public string continueButtonText = "Continue";
public string cancelButtonText = "Cancel";
public string moreInfoUrl = "https://hotreload.net/documentation#handling-different-commits";
}
/// hide this dialog
void Hide() {
gameObject.SetActive(false); // this should disable the Update loop?
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: ef31038a0ed84685b779466bf22d53a9
timeCreated: 1675143382
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/MonoBehaviours/QuestionDialog.cs
uploadId: 752503

View File

@@ -0,0 +1,104 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.UI;
namespace SingularityGroup.HotReload {
internal class RetryDialog : MonoBehaviour {
[Header("UI controls")]
public Button buttonHide;
public Button buttonRetryAutoPair;
public Button buttonTroubleshoot;
public Text textSummary;
public Text textSuggestion;
public InputField ipInput;
[Tooltip("Hidden by default")]
public Text textForDebugging;
[Header("For HotReload Devs")]
// In Unity Editor, click checkbox to see info helpful for debugging bugs
public bool enableDebugging;
// [Header("Other")]
// [Tooltip("Used when your project does not create an EventSystem early enough")]
// public GameObject fallbackEventSystem;
private static RetryDialog _I;
public string DebugInfo {
set {
textForDebugging.text = value;
}
}
public bool autoConnect { get; set; }
void Start() {
buttonHide.onClick.AddListener(() => {
Hide();
});
buttonRetryAutoPair.onClick.AddListener(() => {
Hide();
int port;
var ipAndPort = ipInput.textComponent.text.Split(':');
if (ipAndPort.Length != 2 || !int.TryParse(ipAndPort[1], out port)) {
port = PlayerEntrypoint.PlayerBuildInfo?.buildMachinePort ?? RequestHelper.defaultPort;
}
var ip = ipAndPort.Length > 0 ? ipAndPort[0] : string.Empty;
PlayerEntrypoint.TryConnectToIpAndPort(ip, port);
});
buttonTroubleshoot.onClick.AddListener(() => {
Application.OpenURL("https://hotreload.net/documentation#connection-issues");
});
}
[CanBeNull]
public static PatchServerInfo TargetServer { private get; set; } = null;
public static ServerHandshake.Result HandshakeResults { private get; set; } = ServerHandshake.Result.None;
private void OnEnable() {
ipInput.text = $"{PlayerEntrypoint.PlayerBuildInfo?.buildMachineHostName}:{PlayerEntrypoint.PlayerBuildInfo?.buildMachinePort}";
UpdateUI();
}
void Update() {
UpdateUI();
}
void UpdateUI() {
// assumes that auto-pair already tried for several seconds
// suggestions to help the user when auto-pair is failing
var networkText = Application.isMobilePlatform ? "WiFi" : "LAN/WiFi";
var noWifiNetwork = $"Is this device connected to {networkText}?";
var waitForCompiling = "Wait for compiling to finish before trying again";
var targetNetworkIsReachable = $"Make sure you're on the same {networkText} network. Also ensure Hot Reload is running";
if (Application.internetReachability != NetworkReachability.ReachableViaLocalAreaNetwork) {
textSuggestion.text = noWifiNetwork;
} else if (HandshakeResults.HasFlag(ServerHandshake.Result.WaitForCompiling)) {
// Note: Technically the player could do the waiting itself, and handshake again with the server
// only after compiling finishes... Telling the user to do that is easier to implement though.
textSuggestion.text = waitForCompiling;
} else {
textSuggestion.text = targetNetworkIsReachable;
}
textSummary.text = autoConnect ? "Auto-pair encountered an issue" : "Connection failed";
if (enableDebugging && textForDebugging) {
textForDebugging.enabled = true;
textForDebugging.text = $"the target = {TargetServer}";
}
}
/// hide this dialog
void Hide() {
gameObject.SetActive(false); // this should disable the Update loop?
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 7a69f8e8e50a405a84ec22ac7c2f4bdc
timeCreated: 1674408078
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/MonoBehaviours/RetryDialog.cs
uploadId: 752503

View File

@@ -0,0 +1,196 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Runtime.InteropServices;
namespace SingularityGroup.HotReload.Interop {
//see _MonoMethod struct in class-internals.h
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto, Size = 8 + sizeof(long) * 3 + 4)]
internal unsafe struct MonoMethod64 {
[FieldOffset(0)]
public MethodAttributes flags;
[FieldOffset(2)]
public MethodImplAttributes iflags;
[FieldOffset(4)]
public uint token;
[FieldOffset(8)]
public void* klass;
[FieldOffset(8 + sizeof(long))]
public void* signature;
[FieldOffset(8 + sizeof(long) * 2)]
public char* name;
/* this is used by the inlining algorithm */
[FieldOffset(8 + sizeof(long) * 3)]
public MonoMethodFlags monoMethodFlags;
[FieldOffset(8 + sizeof(long) * 3 + 2)]
public short slot;
}
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto, Size = 8 + sizeof(int) * 3 + 4)]
internal unsafe struct MonoMethod32 {
[FieldOffset(0)]
public MethodAttributes flags;
[FieldOffset(2)]
public MethodImplAttributes iflags;
[FieldOffset(4)]
public uint token;
[FieldOffset(8)]
public void* klass;
[FieldOffset(8 + sizeof(int))]
public void* signature;
[FieldOffset(8 + sizeof(int) * 2)]
public char* name;
/* this is used by the inlining algorithm */
[FieldOffset(8 + sizeof(int) * 3)]
public MonoMethodFlags monoMethodFlags;
[FieldOffset(8 + sizeof(int) * 3 + 2)]
public short slot;
}
//Corresponds to the bitflags of the _MonoMethod struct
[Flags]
internal enum MonoMethodFlags : ushort {
inline_info = 1 << 0, //:1
inline_failure = 1 << 1, //:1
wrapper_type = 1 << 2, //:5
string_ctor = 1 << 7, //:1
save_lmf = 1 << 8, //:1
dynamic = 1 << 9, //:1 /* created & destroyed during runtime */
sre_method = 1 << 10, //:1 /* created at runtime using Reflection.Emit */
is_generic = 1 << 11, //:1 /* whenever this is a generic method definition */
is_inflated = 1 << 12, //:1 /* whether we're a MonoMethodInflated */
skip_visibility = 1 << 13, //:1 /* whenever to skip JIT visibility checks */
verification_success = 1 << 14, //:1 /* whether this method has been verified successfully.*/
}
[Flags]
internal enum MethodImplAttributes : ushort {
/// <summary><para>Specifies that the method implementation is in Microsoft intermediate language (MSIL).</para></summary>
IL = 0,
/// <summary><para>Specifies that the method is implemented in managed code. </para></summary>
Managed = 0,
/// <summary><para>Specifies that the method implementation is native.</para></summary>
Native = 1,
/// <summary><para>Specifies that the method implementation is in Optimized Intermediate Language (OPTIL).</para></summary>
OPTIL = 2,
/// <summary><para>Specifies flags about code type.</para></summary>
CodeTypeMask = 3,
/// <summary><para>Specifies that the method implementation is provided by the runtime.</para></summary>
Runtime = 3,
/// <summary><para>Specifies whether the method is implemented in managed or unmanaged code.</para></summary>
ManagedMask = 4,
/// <summary><para>Specifies that the method is implemented in unmanaged code.</para></summary>
Unmanaged = 4,
/// <summary><para>Specifies that the method cannot be inlined.</para></summary>
NoInlining = 8,
/// <summary><para>Specifies that the method is not defined.</para></summary>
ForwardRef = 16, // 0x00000010
/// <summary><para>Specifies that the method is single-threaded through the body. Static methods (Shared in Visual Basic) lock on the type, whereas instance methods lock on the instance. You can also use the C# <format type="text/html"><a href="656DA1A4-707E-4EF6-9C6E-6D13B646AF42">lock statement</a></format> or the Visual Basic <format type="text/html"><a href="14501703-298f-4d43-b139-c4b6366af176">SyncLock statement</a></format> for this purpose. </para></summary>
Synchronized = 32, // 0x00000020
/// <summary><para>Specifies that the method is not optimized by the just-in-time (JIT) compiler or by native code generation (see <format type="text/html"><a href="44bf97aa-a9a4-4eba-9a0d-cfaa6fc53a66">Ngen.exe</a></format>) when debugging possible code generation problems.</para></summary>
NoOptimization = 64, // 0x00000040
/// <summary><para>Specifies that the method signature is exported exactly as declared.</para></summary>
PreserveSig = 128, // 0x00000080
/// <summary><para>Specifies that the method should be inlined wherever possible.</para></summary>
AggressiveInlining = 256, // 0x00000100
/// <summary><para>Specifies an internal call.</para></summary>
InternalCall = 4096, // 0x00001000
/// <summary><para>Specifies a range check value.</para></summary>
MaxMethodImplVal = 65535, // 0x0000FFFF
}
/// <summary><para>Specifies flags for method attributes. These flags are defined in the corhdr.h file.</para></summary>
[Flags]
internal enum MethodAttributes : ushort {
/// <summary><para>Retrieves accessibility information.</para></summary>
MemberAccessMask = 7,
/// <summary><para>Indicates that the member cannot be referenced.</para></summary>
PrivateScope = 0,
/// <summary><para>Indicates that the method is accessible only to the current class.</para></summary>
Private = 1,
/// <summary><para>Indicates that the method is accessible to members of this type and its derived types that are in this assembly only.</para></summary>
FamANDAssem = 2,
/// <summary><para>Indicates that the method is accessible to any class of this assembly.</para></summary>
Assembly = FamANDAssem | Private, // 0x00000003
/// <summary><para>Indicates that the method is accessible only to members of this class and its derived classes.</para></summary>
Family = 4,
/// <summary><para>Indicates that the method is accessible to derived classes anywhere, as well as to any class in the assembly.</para></summary>
FamORAssem = Family | Private, // 0x00000005
/// <summary><para>Indicates that the method is accessible to any object for which this object is in scope.</para></summary>
Public = Family | FamANDAssem, // 0x00000006
/// <summary><para>Indicates that the method is defined on the type; otherwise, it is defined per instance.</para></summary>
Static = 16, // 0x00000010
/// <summary><para>Indicates that the method cannot be overridden.</para></summary>
Final = 32, // 0x00000020
/// <summary><para>Indicates that the method is virtual.</para></summary>
Virtual = 64, // 0x00000040
/// <summary><para>Indicates that the method hides by name and signature; otherwise, by name only.</para></summary>
HideBySig = 128, // 0x00000080
/// <summary><para>Indicates that the method can only be overridden when it is also accessible.</para></summary>
CheckAccessOnOverride = 512, // 0x00000200
/// <summary><para>Retrieves vtable attributes.</para></summary>
VtableLayoutMask = 256, // 0x00000100
/// <summary><para>Indicates that the method will reuse an existing slot in the vtable. This is the default behavior.</para></summary>
ReuseSlot = 0,
/// <summary><para>Indicates that the method always gets a new slot in the vtable.</para></summary>
NewSlot = VtableLayoutMask, // 0x00000100
/// <summary><para>Indicates that the class does not provide an implementation of this method.</para></summary>
Abstract = 1024, // 0x00000400
/// <summary><para>Indicates that the method is special. The name describes how this method is special.</para></summary>
SpecialName = 2048, // 0x00000800
/// <summary><para>Indicates that the method implementation is forwarded through PInvoke (Platform Invocation Services).</para></summary>
PinvokeImpl = 8192, // 0x00002000
/// <summary><para>Indicates that the managed method is exported by thunk to unmanaged code.</para></summary>
UnmanagedExport = 8,
/// <summary><para>Indicates that the common language runtime checks the name encoding.</para></summary>
RTSpecialName = 4096, // 0x00001000
/// <summary><para>Indicates a reserved flag for runtime use only.</para></summary>
ReservedMask = 53248, // 0x0000D000
/// <summary><para>Indicates that the method has security associated with it. Reserved flag for runtime use only.</para></summary>
HasSecurity = 16384, // 0x00004000
/// <summary><para>Indicates that the method calls another method containing security code. Reserved flag for runtime use only.</para></summary>
RequireSecObject = 32768, // 0x00008000
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: bb9456a28c3b8644e9b0f78eb6d9ac17
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/MonoMethod.cs
uploadId: 752503

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 62fd71232a4784cdeb53a6ab67694087
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,87 @@
fileFormatVersion: 2
guid: 56a90f73ab41d4145bb173186644f641
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 0
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: OSX
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 1
settings:
CPU: AnyCPU
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: x86
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: x86_64
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/OSX/HotReloadNativeHelper.dylib
uploadId: 752503

View File

@@ -0,0 +1,194 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
#pragma warning disable CS0618 // obsolete warnings (stay warning-free also in newer unity versions)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SingularityGroup.HotReload {
static class Dispatch {
// DispatchOnHotReload is called every time a patch is applied (1x per batch of filechanges)
public static async Task OnHotReload(List<MethodPatch> patchedMethods) {
var methods = await Task.Run(() => GetOrFillMethodsCacheThreaded());
foreach (var m in methods) {
if (m.IsStatic) {
InvokeStaticMethod(m, nameof(InvokeOnHotReload), patchedMethods);
} else {
foreach (var go in GameObject.FindObjectsOfType(m.DeclaringType)) {
InvokeInstanceMethod(m, go, patchedMethods);
}
}
}
}
public static void OnHotReloadLocal(MethodBase originalMethod, MethodBase patchMethod) {
if (!Attribute.IsDefined(originalMethod, typeof(InvokeOnHotReloadLocal))) {
return;
}
var attrib = Attribute.GetCustomAttribute(originalMethod, typeof(InvokeOnHotReloadLocal)) as InvokeOnHotReloadLocal;
if (!string.IsNullOrEmpty(attrib?.methodToInvoke)) {
OnHotReloadLocalCustom(originalMethod, attrib);
return;
}
var patchMethodParams = patchMethod.GetParameters();
if (patchMethodParams.Length == 0) {
InvokeStaticMethod(patchMethod, nameof(InvokeOnHotReloadLocal), null);
} else if (typeof(MonoBehaviour).IsAssignableFrom(patchMethodParams[0].ParameterType)) {
foreach (var go in GameObject.FindObjectsOfType(patchMethodParams[0].ParameterType)) {
InvokeInstanceMethodStatic(patchMethod, go);
}
} else {
Log.Warning($"[{nameof(InvokeOnHotReloadLocal)}] {patchMethod.DeclaringType?.Name} {patchMethod.Name} failed. Make sure it's a method with 0 parameters either static or defined on MonoBehaviour.");
}
}
public static void OnHotReloadLocalCustom(MethodBase origianlMethod, InvokeOnHotReloadLocal attrib) {
var reloadForType = origianlMethod.DeclaringType;
var reloadMethod = reloadForType?.GetMethod(attrib.methodToInvoke, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (reloadMethod == null) {
Log.Warning($"[{nameof(InvokeOnHotReloadLocal)}] failed to find method {attrib.methodToInvoke}. Make sure it exists within the same class.");
return;
}
if (reloadMethod.IsStatic) {
InvokeStaticMethod(reloadMethod, nameof(InvokeOnHotReloadLocal), null);
} else if (typeof(MonoBehaviour).IsAssignableFrom(reloadForType)) {
foreach (var go in GameObject.FindObjectsOfType(reloadForType)) {
InvokeInstanceMethod(reloadMethod, go, null);
}
} else {
Log.Warning($"[{nameof(InvokeOnHotReloadLocal)}] {reloadMethod.DeclaringType?.Name} {reloadMethod.Name} failed. Make sure it's a method with 0 parameters either static or defined on MonoBehaviour.");
}
}
private static List<MethodInfo> methodsCache;
private static List<MethodInfo> GetOrFillMethodsCacheThreaded() {
if (methodsCache != null) {
return methodsCache;
}
#if UNITY_2019_1_OR_NEWER && UNITY_EDITOR
var methodCollection = UnityEditor.TypeCache.GetMethodsWithAttribute(typeof(InvokeOnHotReload));
var methods = new List<MethodInfo>();
foreach (var m in methodCollection) {
methods.Add(m);
}
#else
var methods = GetMethodsReflection();
#endif
methodsCache = methods;
return methods;
}
private static List<MethodInfo> GetMethodsReflection() {
var methods = new List<MethodInfo>();
try {
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) {
if (asm.FullName == "System" || asm.FullName.StartsWith("System.", StringComparison.Ordinal)) {
continue; // big performance optimization
}
try {
foreach (var type in asm.GetTypes()) {
try {
foreach (var m in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) {
try {
if (Attribute.IsDefined(m, typeof(InvokeOnHotReload))) {
methods.Add(m);
}
} catch (BadImageFormatException) {
// silently ignore (can happen, is very annoying if it spams)
/*
BadImageFormatException: VAR 3 (TOutput) cannot be expanded in this context with 3 instantiations
System.Reflection.MonoMethod.GetBaseMethod () (at <c8d0d7b9135640958bff528a1e374758>:0)
System.MonoCustomAttrs.GetBase (System.Reflection.ICustomAttributeProvider obj) (at <c8d0d7b9135640958bff528a1e374758>:0)
System.MonoCustomAttrs.IsDefined (System.Reflection.ICustomAttributeProvider obj, System.Type attributeType, System.Boolean inherit) (at <c8d0d7b9135640958bff528a1e374758>:0)
*/
} catch (TypeLoadException) {
// silently ignore (can happen, is very annoying if it spams)
} catch (Exception e) {
ThreadUtility.LogException(new AggregateException(type.Name + "." + m.Name, e));
}
}
} catch (BadImageFormatException) {
// silently ignore (can happen, is very annoying if it spams)
} catch (TypeLoadException) {
// silently ignore (can happen, is very annoying if it spams)
} catch (Exception e) {
ThreadUtility.LogException(new AggregateException(type.Name, e));
}
}
} catch (BadImageFormatException) {
// silently ignore (can happen, is very annoying if it spams)
} catch (TypeLoadException) {
// silently ignore (can happen, is very annoying if it spams)
} catch (Exception e) {
ThreadUtility.LogException(new AggregateException(asm.FullName, e));
}
}
} catch (Exception e) {
ThreadUtility.LogException(e);
}
return methods;
}
private static void InvokeStaticMethod(MethodBase m, string attrName, List<MethodPatch> patchedMethods) {
try {
if (patchedMethods != null && m.GetParameters().Length == 1) {
m.Invoke(null, new object[] { patchedMethods });
} else {
m.Invoke(null, new object[] { });
}
} catch (Exception e) {
if (m.GetParameters().Length != 0) {
Log.Warning($"[{attrName}] {m.DeclaringType?.Name} {m.Name} failed. Make sure it has 0 parameters, or 1 parameter with type List<MethodPatch>. Exception:\n{e}");
} else {
Log.Warning($"[{attrName}] {m.DeclaringType?.Name} {m.Name} failed. Exception\n{e}");
}
}
}
private static void InvokeInstanceMethod(MethodBase m, Object go, List<MethodPatch> patchedMethods) {
try {
if (patchedMethods != null && m.GetParameters().Length == 1) {
m.Invoke(go, new object[] { patchedMethods });
} else {
m.Invoke(go, new object[] { });
}
} catch (Exception e) {
if (m.GetParameters().Length != 0) {
Log.Warning($"[InvokeOnHotReload] {m.DeclaringType?.Name} {m.Name} failed. Make sure it has 0 parameters, or 1 parameter with type List<MethodPatch>. Exception:\n{e}");
} else {
Log.Warning($"[InvokeOnHotReload] {m.DeclaringType?.Name} {m.Name} failed. Exception:\n{e}");
}
}
}
private static void InvokeInstanceMethodStatic(MethodBase m, Object go) {
try {
m.Invoke(null, new object[] { go });
} catch (Exception e) {
if (m.GetParameters().Length != 0) {
Log.Warning($"[InvokeOnHotReloadLocal] {m.DeclaringType?.Name} {m.Name} failed. Make sure it has 0 parameters. Exception:\n{e}");
} else {
Log.Warning($"[InvokeOnHotReloadLocal] {m.DeclaringType?.Name} {m.Name} failed. Exception:\n{e}");
}
}
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 8fde7dfa45d340eda4c6c64ccf52e17d
timeCreated: 1673824017
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/OnHotReloadDispatch.cs
uploadId: 752503

View File

@@ -0,0 +1,20 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using UnityEngine;
namespace SingularityGroup.HotReload {
internal static class PackageConst {
//CI changes this property to 'true' for asset store builds.
//Don't touch unless you know what you are doing
public static bool IsAssetStoreBuild => true;
public const string Version = "1.13.7";
// Never higher than Version
// Used for the download
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";
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 877e785c22c57be45a4d79c1be346c79
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/PackageConst.cs
uploadId: 752503

View File

@@ -0,0 +1,41 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using SingularityGroup.HotReload.Newtonsoft.Json;
using UnityEngine;
namespace SingularityGroup.HotReload {
[Serializable]
class PatchServerInfo {
public readonly string hostName;
public readonly int port;
public readonly string commitHash;
public readonly string rootPath;
[Obsolete]public readonly bool isRemote;
public readonly string customRequestOrigin;
public const string UnknownCommitHash = "unknown";
/// <param name="hostName">an ip address or "localhost"</param>
public PatchServerInfo(string hostName, string commitHash, string rootPath) {
this.hostName = hostName;
this.commitHash = commitHash ?? UnknownCommitHash;
this.rootPath = rootPath;
this.port = RequestHelper.defaultPort;
}
/// <param name="hostName">an ip address or "localhost"</param>
// constructor should (must?) have a param for each field
[JsonConstructor]
public PatchServerInfo(string hostName, int port, string commitHash, string rootPath, bool isRemote = false, string customRequestOrigin = null) {
this.hostName = hostName;
this.port = port;
this.commitHash = commitHash ?? UnknownCommitHash;
this.rootPath = rootPath;
#pragma warning disable CS0612 // Type or member is obsolete
this.isRemote = isRemote;
#pragma warning restore CS0612 // Type or member is obsolete
this.customRequestOrigin = customRequestOrigin;
}
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 5795d77991613ae48870b7c349491adc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/PatchServerInfo.cs
uploadId: 752503

View File

@@ -0,0 +1,15 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System.IO;
namespace SingularityGroup.HotReload {
static class PersistencePaths {
public static string GetPatchesFilePath(string basePath) {
return Path.Combine(basePath, "CodePatcher", "patches.bin");
}
public static string GetServerInfoFilePath(string basePath) {
return Path.Combine(basePath, "CodePatcher", "hostInfo.json");
}
}
}
#endif

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 3742023437057a049842c5fad1019132
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/PersistencePaths.cs
uploadId: 752503

View File

@@ -0,0 +1,117 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
using System;
using System.Threading;
using System.Threading.Tasks;
using SingularityGroup.HotReload.DTO;
namespace SingularityGroup.HotReload {
static class PlayerCodePatcher {
static Timer timer;
static PlayerCodePatcher() {
if (PlayerEntrypoint.IsPlayerWithHotReload()) {
timer = new Timer(OnIntervalThreaded, (Action) OnIntervalMainThread, 500, 500);
serverHealthyAt = DateTime.MinValue;
}
}
private static DateTime serverHealthyAt;
private static TimeSpan TimeSinceServerHealthy() => DateTime.UtcNow - serverHealthyAt;
/// <summary>
/// Set server that you want to try connect to.
/// </summary>
/// <remarks>
/// <para>
/// This allows repetitions of:
/// - try handshake
/// - success -> try healthcheck
/// - success -> poll method patches
/// -
/// </para>
/// <para>
/// Only do this after confirming (with /handshake) that server is compatible with this build.<br/>
/// The user will be prompted if handshake needs confirmation.
/// </para>
/// </remarks>
internal static Task<ServerHandshake.Result> UpdateHost(PatchServerInfo serverInfo) {
Log.Debug($"UpdateHost to {(serverInfo == null ? "null" : serverInfo.hostName)}");
// In player builds, server is remote, se we don't load assemblies from any paths
RequestHelper.ChangeAssemblySearchPaths(Array.Empty<string>());
ServerHealthCheck.I.SetServerInfo(null); // stop doing health check on old server
RequestHelper.SetServerInfo(serverInfo);
// Show feedback about connection progress (handshake can take ~5 seconds for our big game)
if (serverInfo == null) {
Prompts.SetConnectionState(ConnectionSummary.Disconnected);
} else {
Prompts.SetConnectionState(ConnectionSummary.Connected);
Prompts.ShowConnectionDialog();
}
return ServerHandshake.I.SetServerInfo(serverInfo);
}
public static Task Disconnect() => UpdateHost(null);
static void OnIntervalThreaded(object o) {
ServerHandshake.I.CheckHandshake();
ServerHealthCheck.I.CheckHealthAsync().Forget();
ThreadUtility.RunOnMainThread((Action)o);
}
static string lastPatchId = string.Empty;
static void OnIntervalMainThread() {
PatchServerInfo verifiedServer;
if(ServerHandshake.I.TryGetVerifiedServer(out verifiedServer)) {
// now that handshake verified, we are connected.
// Note: If there is delay between handshake done and chosing to connect, then it may be outdated.
Prompts.SetConnectionState(ConnectionSummary.Connecting);
// Note: verified does not imply that server is running, sometimes we verify the host just from the deeplink data
ServerHealthCheck.I.SetServerInfo(verifiedServer);
}
if(ServerHealthCheck.I.IsServerHealthy) {
// we may have reconnected to the same host, after losing connection for several seconds
Prompts.SetConnectionState(ConnectionSummary.Connected, false);
serverHealthyAt = DateTime.UtcNow;
RequestHelper.PollMethodPatches(lastPatchId, resp => HandleResponseReceived(resp));
} else if (ServerHealthCheck.I.WasServerResponding) { // only update prompt state if disconnected server
var secondsSinceHealthy = TimeSinceServerHealthy().TotalSeconds;
var reconnectTimeout = 30; // seconds
if (secondsSinceHealthy > 2) {
Log.Info("Hot Reload was unreachable for 5 seconds, trying to reconnect...");
// feedback for the user so they know why patches are not applying
Prompts.SetConnectionState($"{ConnectionSummary.TryingToReconnect} {reconnectTimeout - secondsSinceHealthy:F0}s", false);
Prompts.ShowConnectionDialog();
}
if (secondsSinceHealthy > reconnectTimeout) {
// give up on the server, give user a way to connect to another
Log.Info($"Hot Reload was unreachable for {reconnectTimeout} seconds, disconnecting");
var disconnectedServer = RequestHelper.ServerInfo;
Disconnect().Forget();
// Let user tap button to retry connecting to the same server (maybe just need to run Hot Reload again)
// Assumption: prompt also has a way to connect to a different server
Prompts.ShowRetryDialog(disconnectedServer);
}
}
}
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);
if(response.patches.Length > 0) {
CodePatcher.I.RegisterPatches(response, persist: true);
}
if(response.failures.Length > 0) {
foreach (var failure in response.failures) {
// feedback to user so they know why their patch wasn't applied
Log.Warning(failure);
}
}
lastPatchId = response.id;
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 41b853a967284f44b36c071abf30124c
timeCreated: 1674453262
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/PlayerCodePatcher.cs
uploadId: 752503

View File

@@ -0,0 +1,161 @@
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
#if UNITY_ANDROID && !UNITY_EDITOR
#define MOBILE_ANDROID
#endif
#if UNITY_IOS && !UNITY_EDITOR
#define MOBILE_IOS
#endif
#if MOBILE_ANDROID || MOBILE_IOS
#define MOBILE
#endif
using System;
using System.Threading.Tasks;
#if MOBILE_ANDROID
// not able to use File apis for reading from StreamingAssets
using UnityEngine.Networking;
#endif
using UnityEngine;
using Debug = UnityEngine.Debug;
using System.IO;
namespace SingularityGroup.HotReload {
// entrypoint for Unity Player builds. Not necessary in Unity Editor.
internal static class PlayerEntrypoint {
/// Set when behaviour is created, when you access this instance through the singleton,
/// you can assume that this field is not null.
/// <remarks>
/// In Player code you can assume this is set.<br/>
/// When in Editor this is usually null.
/// </remarks>
static BuildInfo buildInfo { get; set; }
/// In Player code you can assume this is set (not null)
public static BuildInfo PlayerBuildInfo => buildInfo;
#if ENABLE_MONO
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
#endif
private static void InitOnAppLoad() {
AppCallbackListener.Init(); // any platform might be using this
UnityHelper.Init();
bool onlyPrefabMissing;
if (!IsPlayerWithHotReload(out onlyPrefabMissing)) {
if (onlyPrefabMissing) {
Log.Warning("Hot Reload is not available in this build because one or more build settings were not supported.");
}
return;
}
TryAutoConnect().Forget();
}
static async Task TryAutoConnect() {
try {
buildInfo = await GetBuildInfo();
} catch (Exception e) {
if (e is IOException) {
Log.Warning("Hot Reload is not available in this build because one or more build settings were not supported.");
} else {
Log.Error($"Uknown exception happened when reading build info\n{e.GetType().Name}: {e.Message}");
}
return;
}
if (buildInfo == null) {
Log.Error($"Uknown issue happened when reading build info.");
return;
}
CodePatcher.I.debuggerCompatibilityEnabled = true;
try {
var customIp = PlayerPrefs.GetString("HotReloadRuntime.CustomIP", "");
if (!string.IsNullOrEmpty(customIp)) {
buildInfo.buildMachineHostName = customIp;
}
var customPort = PlayerPrefs.GetString("HotReloadRuntime.CustomPort", "");
if (!string.IsNullOrEmpty(customPort)) {
buildInfo.buildMachinePort = int.Parse(customPort);
}
if (buildInfo.BuildMachineServer == null) {
Prompts.ShowRetryDialog(null);
} else {
// try reach server running on the build machine.
TryConnect(buildInfo.BuildMachineServer, auto: true).Forget();
}
} catch (Exception ex) {
Log.Exception(ex);
}
}
public static Task TryConnectToIpAndPort(string ip, int port) {
ip = ip.Trim();
if (buildInfo == null) {
throw new ArgumentException("Build info not found");
}
buildInfo.buildMachineHostName = ip;
buildInfo.buildMachinePort = port;
PlayerPrefs.SetString("HotReloadRuntime.CustomIP", ip);
PlayerPrefs.SetString("HotReloadRuntime.CustomPort", port.ToString());
return TryConnect(buildInfo.BuildMachineServer, auto: false);
}
public static async Task TryConnect(PatchServerInfo serverInfo, bool auto) {
// try reach server running on the build machine.
var handshake = PlayerCodePatcher.UpdateHost(serverInfo);
await Task.WhenAny(handshake, Task.Delay(TimeSpan.FromSeconds(40)));
await ThreadUtility.SwitchToMainThread();
var handshakeResults = await handshake;
var handshakeOk = handshakeResults.HasFlag(ServerHandshake.Result.Verified);
if (!handshakeOk) {
Log.Debug("ShowRetryPrompt because handshake result is {0}", handshakeResults);
Prompts.ShowRetryDialog(serverInfo, handshakeResults, auto);
// cancel trying to connect. They can use the retry button
PlayerCodePatcher.UpdateHost(null).Forget();
}
Log.Info($"Server is healthy after first handshake? {handshakeOk}");
}
/// on Android, streaming assets are inside apk zip, which can only be read using unity web request
private static async Task<BuildInfo> GetBuildInfo() {
var path = BuildInfo.GetStoredPath();
#if MOBILE_ANDROID
var json = await RequestHelper.GetAsync(path);
return await Task.Run(() => BuildInfo.FromJson(json));
#else
return await Task.Run(() => {
return BuildInfo.FromJson(File.ReadAllText(path));
});
#endif
}
public static bool IsPlayer() => !Application.isEditor;
public static bool IsPlayerWithHotReload() {
bool _;
return IsPlayerWithHotReload(out _);
}
public static bool IsPlayerWithHotReload(out bool onlyPrefabMissing) {
onlyPrefabMissing = false;
if (!IsPlayer() || !RuntimeSupportsHotReload || !HotReloadSettingsObject.I.IncludeInBuild) {
return false;
}
onlyPrefabMissing = !HotReloadSettingsObject.I.PromptsPrefab;
return !onlyPrefabMissing;
}
public static bool RuntimeSupportsHotReload {
get {
#if DEVELOPMENT_BUILD && ENABLE_MONO
return true;
#else
return false;
#endif
}
}
}
}
#endif

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 79392e60063749df984beec2908685bf
timeCreated: 1674215564
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/PlayerEntrypoint.cs
uploadId: 752503

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: da163599826f56e4f929d76e8e35c7e2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
namespace SingularityGroup.HotReload {
/// <summary>
/// Utility class to set the log level of the Hot Reload package
/// </summary>
public static class HotReloadLogging {
/// <summary>
/// Sets the log level for logs inside the Hot Reload package
/// The default log level is <see cref="LogLevel.Info"/>
/// </summary>
/// <remarks>
/// To see more detailed logs, set the log level to <see cref="LogLevel.Debug"/>
/// </remarks>
public static void SetLogLevel(LogLevel level) {
Log.minLevel = level;
}
}
}

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 48845e010a33e2d4993888d9c18e8d95
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Public/HotReloadLogging.cs
uploadId: 752503

View File

@@ -0,0 +1,32 @@
using System;
using System.Reflection;
namespace SingularityGroup.HotReload {
/// <summary>
/// Methods with this attribute will get invoked after a hot reload
/// </summary>
/// <remarks>
/// The method with this attribute needs to have no parameters.
/// Further more it needs to either be static or and instance method inside a <see cref="UnityEngine.MonoBehaviour"/>.
/// For the latter case the method of all instances of the <see cref="UnityEngine.MonoBehaviour"/> will be called.
/// In case the method has a return value it will be ignored.
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
public class InvokeOnHotReload : Attribute {
}
public class MethodPatch {
// Compiled by the Unity Editor. Null for newly added methods.
public MethodBase originalMethod;
// Method before Hot Reload applied it's patch. Null for newly added methods.
public MethodBase previousMethod;
// Method generated by Hot Reload.
public MethodBase newMethod;
public MethodPatch(MethodBase originalMethod, MethodBase previousMethod, MethodBase newMethod) {
this.originalMethod = originalMethod;
this.previousMethod = previousMethod;
this.newMethod = newMethod;
}
}
}

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: de8fb6e6e05ccae46a410932e1545848
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Public/InvokeOnHotReload.cs
uploadId: 752503

View File

@@ -0,0 +1,22 @@
using System;
namespace SingularityGroup.HotReload {
/// <summary>
/// Method with this attribute will get invoked when it gets patched
/// </summary>
/// <remarks>
/// The method with this attribute needs to have no parameters.
/// Furthermore it needs to either be static or an instance method inside a <see cref="UnityEngine.MonoBehaviour"/>.
/// For the latter case the method of all instances of the <see cref="UnityEngine.MonoBehaviour"/> will be called.
/// In case the method has a return value it will be ignored.
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
public class InvokeOnHotReloadLocal : Attribute {
public readonly string methodToInvoke;
public InvokeOnHotReloadLocal(string methodToInvoke = null) {
this.methodToInvoke = methodToInvoke;
}
}
}

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f4629f348dad47d4add28eca71be748c
timeCreated: 1694274524
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Public/InvokeOnHotReloadLocal.cs
uploadId: 752503

View File

@@ -0,0 +1,108 @@
using System;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
#if (UNITY_2019_4_OR_NEWER)
using UnityEngine;
#endif
namespace SingularityGroup.HotReload {
public static class Log {
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global")]
public static LogLevel minLevel = LogLevel.Info;
/// <summary>
/// Tag every log so that users know which logs came from Hot Reload
/// </summary>
private const string TAG = "[HotReload] ";
public static void Debug(string message) {
if (minLevel <= LogLevel.Debug) {
#if (UNITY_2019_4_OR_NEWER)
UnityEngine.Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "{0}{1}", TAG, message);
#else
UnityEngine.Debug.Log(TAG + message);
#endif
}
}
[StringFormatMethod("message")]
public static void Debug(string message, params object[] args) {
if (minLevel <= LogLevel.Debug) {
#if (UNITY_2019_4_OR_NEWER)
UnityEngine.Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, TAG + message, args);
#else
UnityEngine.Debug.LogFormat(TAG + message, args);
#endif
}
}
public static void Info(string message) {
if (minLevel <= LogLevel.Info) {
#if (UNITY_2019_4_OR_NEWER)
UnityEngine.Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "{0}{1}", TAG, message);
#else
UnityEngine.Debug.Log(TAG + message);
#endif
}
}
[StringFormatMethod("message")]
public static void Info(string message, params object[] args) {
if (minLevel <= LogLevel.Info) {
#if (UNITY_2019_4_OR_NEWER)
UnityEngine.Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, TAG + message, args);
#else
UnityEngine.Debug.LogFormat(TAG + message, args);
#endif
}
}
public static void Warning(string message) {
if (minLevel <= LogLevel.Warning) {
#if (UNITY_2019_4_OR_NEWER)
UnityEngine.Debug.LogFormat(LogType.Warning, LogOption.NoStacktrace, null, "{0}{1}", TAG, message);
#else
UnityEngine.Debug.LogWarning(TAG + message);
#endif
}
}
[StringFormatMethod("message")]
public static void Warning(string message, params object[] args) {
if (minLevel <= LogLevel.Warning) {
#if (UNITY_2019_4_OR_NEWER)
UnityEngine.Debug.LogFormat(LogType.Warning, LogOption.NoStacktrace, null, TAG + message, args);
#else
UnityEngine.Debug.LogWarningFormat(TAG + message, args);
#endif
}
}
public static void Error(string message) {
if (minLevel <= LogLevel.Error) {
#if (UNITY_2019_4_OR_NEWER)
UnityEngine.Debug.LogFormat(LogType.Error, LogOption.NoStacktrace, null, "{0}{1}", TAG, message);
#else
UnityEngine.Debug.LogError(TAG + message);
#endif
}
}
[StringFormatMethod("message")]
public static void Error(string message, params object[] args) {
if (minLevel <= LogLevel.Error) {
#if (UNITY_2019_4_OR_NEWER)
UnityEngine.Debug.LogFormat(LogType.Error, LogOption.NoStacktrace, null, TAG + message, args);
#else
UnityEngine.Debug.LogErrorFormat(TAG + message, args);
#endif
}
}
public static void Exception(Exception exception) {
if (minLevel <= LogLevel.Exception) {
UnityEngine.Debug.LogException(exception);
}
}
}
}

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 0719c57eeba24f4cac8d99016ba5b967
timeCreated: 1674203567
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Public/Log.cs
uploadId: 752503

View File

@@ -0,0 +1,25 @@
namespace SingularityGroup.HotReload {
/// <summary>
/// The log level enumeration for the Hot Reload package
/// Used in <see cref="HotReloadLogging.SetLogLevel"/> to set the log level.
/// </summary>
public enum LogLevel {
/// Debug logs are useful for developers of Hot Reload
Debug = 1,
/// Info logs potentially useful for users of Hot Reload
Info = 2,
/// Warnings are visible to users of Hot Reload
Warning = 3,
/// Errors are visible to users of Hot Reload
Error = 4,
/// Exceptions are visible to users of Hot Reload
Exception = 5,
/// No logs are visible to users of Hot Reload
Disabled = 6,
}
}

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 7b5723ec3c2140041a9c25bbf16eeac4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 254358
packageName: Hot Reload | Edit Code Without Compiling
packageVersion: 1.13.7
assetPath: Packages/com.singularitygroup.hotreload/Runtime/Public/LogLevel.cs
uploadId: 752503

Some files were not shown because too many files have changed in this diff Show More