mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-05 12:58:22 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
943 lines
24 KiB
C#
943 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading.Tasks;
|
|
using Sandbox.Upgraders;
|
|
using Sentry;
|
|
|
|
namespace Sandbox
|
|
{
|
|
public partial class Hotload
|
|
{
|
|
private readonly Dictionary<Type, Type> SubstituteTypeCache = new Dictionary<Type, Type>();
|
|
private readonly Dictionary<Type, InstanceTimingEntry> TypeTimings = new Dictionary<Type, InstanceTimingEntry>();
|
|
private readonly Dictionary<IInstanceProcessor, InstanceTimingEntry> ProcessorTimings = new Dictionary<IInstanceProcessor, InstanceTimingEntry>();
|
|
|
|
private readonly Queue<InstanceTask> DefaultInstanceTaskQueue = new Queue<InstanceTask>();
|
|
private readonly Queue<InstanceTask> LateInstanceTaskQueue = new Queue<InstanceTask>();
|
|
|
|
private readonly Dictionary<Assembly, (Type Type, FieldInfo[] Fields)[]> StaticFieldCache = new();
|
|
|
|
private HotloadResult CurrentResult;
|
|
|
|
private ReferencePath CurrentPath;
|
|
private FieldInfo CurrentSrcField;
|
|
private FieldInfo CurrentDstField;
|
|
|
|
/// <summary>
|
|
/// Merge chains of swaps. For example, if A is swapped with B, and B is swapped with C, simplify to A swapping with C.
|
|
/// </summary>
|
|
private void SimplifySwaps()
|
|
{
|
|
foreach ( var oldAsm in Swaps.Keys.ToArray() )
|
|
{
|
|
var newAsm = Swaps[oldAsm];
|
|
var count = Swaps.Count;
|
|
|
|
while ( newAsm != null && Swaps.TryGetValue( newAsm, out var nextAsm ) )
|
|
{
|
|
if ( count-- < 0 )
|
|
{
|
|
Log( HotloadEntryType.Error, $"Assembly swap cycle detected involving {FormatAssemblyName( oldAsm )}." );
|
|
newAsm = oldAsm;
|
|
break;
|
|
}
|
|
|
|
newAsm = nextAsm;
|
|
}
|
|
|
|
Swaps[oldAsm] = newAsm;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cycle though all types in all watched assemblies.
|
|
/// Find statics, iterate over all their fields recursively.
|
|
/// Replace any instances of classes that are defined in the assemblies added using ReplacingAssembly
|
|
/// </summary>
|
|
public HotloadResult UpdateReferences()
|
|
{
|
|
// If there's nothing to do, early out
|
|
if ( Swaps.Count == 0 )
|
|
return HotloadResult.NoActionSingleton;
|
|
|
|
CurrentResult = new HotloadResult();
|
|
|
|
TypeTimings.Clear();
|
|
ProcessorTimings.Clear();
|
|
|
|
var timer = new Stopwatch();
|
|
timer.Start();
|
|
|
|
SimplifySwaps();
|
|
|
|
// Remove cached static fields for assemblies that aren't watched any more
|
|
foreach ( var cachedAsm in StaticFieldCache.Keys.ToArray() )
|
|
{
|
|
if ( Swaps.ContainsKey( cachedAsm ) || !_watchedAssemblies.ContainsKey( cachedAsm ) )
|
|
{
|
|
StaticFieldCache.Remove( cachedAsm );
|
|
}
|
|
}
|
|
|
|
// Make sure instance upgraders are set up
|
|
foreach ( var upgrader in AllUpgraders.Values )
|
|
{
|
|
if ( !upgrader.IsInitialized )
|
|
upgrader.Initialize( this );
|
|
}
|
|
|
|
var defaultUpgrader = GetUpgrader<DefaultUpgrader>();
|
|
|
|
try
|
|
{
|
|
RootUpgraderGroup.HotloadStart();
|
|
|
|
var staticFieldStartTime = timer.Elapsed;
|
|
var watchedAssemblies = _watchedAssemblies.Keys
|
|
.Union( Swaps.Keys )
|
|
.Union( New.Where( x => !Swaps.ContainsValue( x ) ) );
|
|
|
|
foreach ( var (type, fields) in watchedAssemblies.SelectMany( GetWatchedFields ) )
|
|
{
|
|
UpdateReferencesInType( type, fields );
|
|
}
|
|
|
|
CurrentResult.StaticFieldTime = (timer.Elapsed - staticFieldStartTime).TotalMilliseconds;
|
|
|
|
var watchedInstanceStartTime = timer.Elapsed;
|
|
|
|
foreach ( var instance in WatchedInstances )
|
|
{
|
|
CurrentPath = ReferencePath.GetRoot( instance.GetType() );
|
|
defaultUpgrader.ProcessObjectFields( instance );
|
|
CurrentPath = null;
|
|
}
|
|
|
|
CurrentResult.WatchedInstanceTime = (timer.Elapsed - watchedInstanceStartTime).TotalMilliseconds;
|
|
|
|
var instanceQueueStartTime = timer.Elapsed;
|
|
|
|
ProcessInstanceQueue();
|
|
|
|
CurrentResult.InstanceQueueTime = (timer.Elapsed - instanceQueueStartTime).TotalMilliseconds;
|
|
}
|
|
finally
|
|
{
|
|
RootUpgraderGroup.HotloadComplete();
|
|
|
|
if ( TryGetUpgrader<AutoSkipUpgrader>( out var upgrader ) )
|
|
{
|
|
CurrentResult.AutoSkippedTypes.AddRange( upgrader
|
|
.SkippedTypes
|
|
.Select( x => x.ToSimpleString() ) );
|
|
}
|
|
|
|
ClearFieldDefaults();
|
|
DefaultInstanceTaskQueue.Clear();
|
|
LateInstanceTaskQueue.Clear();
|
|
SubstituteTypeCache.Clear();
|
|
ScopeMethodOrdinals.Clear();
|
|
AnonymousTypes.Clear();
|
|
RootUpgraderGroup.ClearCache();
|
|
|
|
// update the watched assemblies if we swapped those
|
|
|
|
foreach ( var swap in Swaps )
|
|
{
|
|
if ( swap.Key is null ) continue;
|
|
if ( !_watchedAssemblies.TryGetValue( swap.Key, out var filter ) ) continue;
|
|
|
|
UnwatchAssembly( swap.Key );
|
|
|
|
if ( swap.Value is null ) continue;
|
|
|
|
WatchAssembly( swap.Value, filter );
|
|
}
|
|
|
|
foreach ( var asm in New )
|
|
{
|
|
WatchAssembly( asm );
|
|
}
|
|
|
|
Swaps.Clear();
|
|
New.Clear();
|
|
}
|
|
|
|
CurrentResult.ProcessingTime = timer.Elapsed.TotalMilliseconds;
|
|
|
|
return CurrentResult;
|
|
}
|
|
|
|
private void ScheduleInstanceTask( IInstanceProcessor handler, object oldInstance, object newInstance )
|
|
{
|
|
DefaultInstanceTaskQueue.Enqueue( new InstanceTask( handler, oldInstance, newInstance, CurrentSrcField, CurrentDstField, CurrentPath ) );
|
|
}
|
|
|
|
private void ScheduleLateInstanceTask( IInstanceProcessor handler, object oldInstance, object newInstance )
|
|
{
|
|
LateInstanceTaskQueue.Enqueue( new InstanceTask( handler, oldInstance, newInstance, CurrentSrcField, CurrentDstField, CurrentPath ) );
|
|
}
|
|
|
|
private void ProcessInstanceQueue()
|
|
{
|
|
var metaSw = new Stopwatch();
|
|
var sw = new Stopwatch();
|
|
|
|
while ( DefaultInstanceTaskQueue.TryDequeue( out var task ) || LateInstanceTaskQueue.TryDequeue( out task ) )
|
|
{
|
|
sw.Restart();
|
|
|
|
CurrentPath = task.Path;
|
|
CurrentSrcField = task.SrcField;
|
|
CurrentDstField = task.DstField;
|
|
|
|
var instanceCount = task.Handler.ProcessInstance( task.OldInstance, task.NewInstance );
|
|
var instanceType = task.OldInstance.GetType();
|
|
|
|
CurrentPath = null;
|
|
CurrentSrcField = null;
|
|
CurrentDstField = null;
|
|
|
|
var elapsed = sw.Elapsed;
|
|
|
|
metaSw.Start();
|
|
|
|
CurrentResult.InstancesProcessed += instanceCount;
|
|
|
|
//
|
|
// record timing information
|
|
// this should help diagnose potentially slow classes
|
|
// that maybe don't need hotloading, so you can disable with
|
|
// the Skip attribute
|
|
//
|
|
{
|
|
string rootString = null;
|
|
|
|
if ( IncludeTypeTimings )
|
|
{
|
|
if ( !TypeTimings.TryGetValue( instanceType, out var tt ) )
|
|
{
|
|
var typeName = instanceType.ToSimpleString();
|
|
|
|
// Check for type name collisions
|
|
if ( !CurrentResult.TypeTimings.TryGetValue( typeName, out tt ) )
|
|
{
|
|
CurrentResult.TypeTimings.Add( typeName,
|
|
tt = new InstanceTimingEntry( TraceRoots ) );
|
|
}
|
|
|
|
TypeTimings.Add( instanceType, tt );
|
|
}
|
|
|
|
AddTiming( tt, instanceCount, elapsed, TraceRoots ? task.Path : null, ref rootString );
|
|
}
|
|
|
|
if ( IncludeProcessorTimings )
|
|
{
|
|
if ( !ProcessorTimings.TryGetValue( task.Handler, out var pt ) )
|
|
{
|
|
var typeName = task.Handler.GetType().ToSimpleString();
|
|
|
|
// Check for type name collisions
|
|
if ( !CurrentResult.ProcessorTimings.TryGetValue( typeName, out pt ) )
|
|
{
|
|
CurrentResult.ProcessorTimings.Add( typeName, pt = new InstanceTimingEntry( TraceRoots ) );
|
|
}
|
|
|
|
ProcessorTimings.Add( task.Handler, pt );
|
|
}
|
|
|
|
AddTiming( pt, instanceCount, elapsed, TraceRoots ? task.Path : null, ref rootString );
|
|
}
|
|
}
|
|
|
|
metaSw.Stop();
|
|
}
|
|
|
|
CurrentResult.DiagnosticsTime = metaSw.Elapsed.TotalMilliseconds;
|
|
}
|
|
|
|
private void AddTiming( InstanceTimingEntry entry, int instanceCount, TimeSpan elapsed, ReferencePath path, ref string rootString )
|
|
{
|
|
entry.Instances += instanceCount;
|
|
entry.Milliseconds += elapsed.TotalMilliseconds;
|
|
|
|
if ( TraceRoots && path != null )
|
|
{
|
|
rootString ??= path.Root.ToString();
|
|
|
|
if ( entry.Roots.TryGetValue( rootString, out var rootTiming ) )
|
|
{
|
|
rootTiming.Instances += instanceCount;
|
|
rootTiming.Milliseconds += elapsed.TotalMilliseconds;
|
|
}
|
|
else
|
|
{
|
|
entry.Roots.Add( rootString, new TimingEntry( instanceCount, elapsed ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool IsMatchingName( AssemblyName a, AssemblyName b )
|
|
{
|
|
return string.Equals( a.Name, b.Name, StringComparison.OrdinalIgnoreCase ) &&
|
|
(a.Version?.Equals( b.Version ) ?? false);
|
|
}
|
|
|
|
private IEnumerable<(Type Type, FieldInfo[] Fields)> GetWatchedFields( Assembly asm )
|
|
{
|
|
if ( Swaps.ContainsKey( asm ) )
|
|
{
|
|
return GetWatchedFieldsUncached( asm, true );
|
|
}
|
|
|
|
if ( !New.Contains( asm ) )
|
|
{
|
|
var references = asm.GetReferencedAssemblies();
|
|
|
|
// For assemblies that aren't getting swapped, check for references to swapped assemblies.
|
|
|
|
foreach ( var reference in references )
|
|
{
|
|
var matchingSwap = Swaps.SingleOrDefault( x => IsMatchingName( x.Key.GetName(), reference ) );
|
|
|
|
if ( matchingSwap.Key == null )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( matchingSwap.Value != null && references.Any( x => IsMatchingName( x, matchingSwap.Value.GetName() ) ) )
|
|
{
|
|
// Both the old and new versions of a swapped assembly are referenced by asm.
|
|
// This will probably only happen in Sandbox.Hotload.Test, and it's intentional.
|
|
|
|
Log( HotloadEntryType.Information, $"Both old and new versions of an assembly are referenced by a non-swapped assembly. ({FormatAssemblyName( asm )} references {FormatAssemblyName( matchingSwap.Key )} and {FormatAssemblyName( matchingSwap.Value )})" );
|
|
continue;
|
|
}
|
|
|
|
// A reference to an assembly that's getting swapped was found.
|
|
// This is technically a fault, but we'll let it slide with a message for now since tools relies on it.
|
|
|
|
Log( HotloadEntryType.Information, $"Skipping static fields from a non-swapped assembly that references a swapped assembly. ({FormatAssemblyName( asm )} references {FormatAssemblyName( matchingSwap.Key )})" );
|
|
return Enumerable.Empty<(Type, FieldInfo[])>();
|
|
}
|
|
}
|
|
|
|
if ( StaticFieldCache.TryGetValue( asm, out var cached ) )
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
cached = GetWatchedFieldsUncached( asm, false ).ToArray();
|
|
StaticFieldCache.Add( asm, cached );
|
|
|
|
return cached;
|
|
}
|
|
|
|
private IEnumerable<(Type Type, FieldInfo[] Fields)> GetWatchedFieldsUncached( Assembly asm, bool isFromSwappedAsm )
|
|
{
|
|
if ( IsAssemblyIgnored( asm ) ) yield break;
|
|
|
|
var filter = _watchedAssemblies.GetValueOrDefault( asm );
|
|
|
|
foreach ( var type in asm.GetTypes() )
|
|
{
|
|
if ( filter?.Invoke( type ) is false ) continue;
|
|
|
|
var fields = GetWatchedFields( type, isFromSwappedAsm ).ToArray();
|
|
|
|
if ( fields.Length == 0 ) continue;
|
|
|
|
yield return (type, fields);
|
|
}
|
|
}
|
|
|
|
private IEnumerable<FieldInfo> GetWatchedFields( Type type, bool isFromSwappedAsm )
|
|
{
|
|
var typeInfo = type.GetTypeInfo();
|
|
var skipUpgrader = GetUpgrader<SkipUpgrader>();
|
|
var autoSkipUpgrader = TryGetUpgrader<AutoSkipUpgrader>( out var upgrader ) ? upgrader : null;
|
|
|
|
// Ignore anything in the hotload assembly
|
|
if ( IsAssemblyIgnored( typeInfo.Assembly ) ) yield break;
|
|
|
|
// TODO: Currently cannot find static fields in instances of generic types
|
|
if ( typeInfo.ContainsGenericParameters ) yield break;
|
|
|
|
// Type definition has a SkipAttribute
|
|
if ( typeInfo.GetCustomAttribute<SkipHotloadAttribute>() != null ) yield break;
|
|
|
|
// Attempting to change fields in <PrivateImplementationDetails> will fail
|
|
if ( type.Name == "<PrivateImplementationDetails>" ) yield break;
|
|
|
|
// Skip static compiler generated types (dynamic caches, etc?)
|
|
if ( typeInfo.IsSealed && isFromSwappedAsm && DelegateUpgrader.IsCompilerGenerated( type ) ) yield break;
|
|
|
|
const BindingFlags flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
|
|
|
|
// Iterate all statics
|
|
foreach ( var staticField in type.GetFields( flags ) )
|
|
{
|
|
// Ignore removed or const values
|
|
if ( staticField.IsLiteral ) continue;
|
|
|
|
// Ignore if marked with SkipAttribute
|
|
if ( staticField.HasAttribute<SkipHotloadAttribute>() ) continue;
|
|
|
|
if ( !isFromSwappedAsm && staticField.FieldType.IsSealed )
|
|
{
|
|
if ( skipUpgrader.ShouldProcessType( staticField.FieldType ) ) continue;
|
|
if ( autoSkipUpgrader?.ShouldProcessType( staticField.FieldType ) ?? false ) continue;
|
|
}
|
|
|
|
yield return staticField;
|
|
}
|
|
}
|
|
|
|
internal void UpdateReferencesInType( Type t, FieldInfo[] staticFields )
|
|
{
|
|
var subType = GetNewType( t );
|
|
|
|
// Ignore removed types
|
|
if ( subType == null ) return;
|
|
|
|
// Iterate all statics
|
|
foreach ( var staticField in staticFields )
|
|
{
|
|
var newField = subType != t
|
|
? subType.GetField( staticField.Name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly )
|
|
: staticField;
|
|
|
|
// Ignore removed fields
|
|
if ( newField == null ) continue;
|
|
|
|
// Ignore const values
|
|
if ( newField.IsLiteral ) continue;
|
|
|
|
// Ignore if marked with SkipAttribute
|
|
if ( newField.HasAttribute<SkipHotloadAttribute>() ) continue;
|
|
|
|
object curVal;
|
|
|
|
try
|
|
{
|
|
curVal = staticField.GetValue( null );
|
|
}
|
|
catch ( TargetInvocationException e )
|
|
{
|
|
if ( e.InnerException is TypeInitializationException )
|
|
{
|
|
Log( HotloadEntryType.Warning, $"{e.InnerException}", t );
|
|
break;
|
|
}
|
|
|
|
Log( e.InnerException, member: t );
|
|
continue;
|
|
}
|
|
|
|
if ( curVal is null ) continue;
|
|
|
|
if ( staticField.TryGetBackedProperty( out var staticProperty ) )
|
|
{
|
|
CurrentPath = ReferencePath.GetRoot( staticProperty );
|
|
}
|
|
else
|
|
{
|
|
CurrentPath = ReferencePath.GetRoot( staticField );
|
|
}
|
|
|
|
CurrentSrcField = staticField;
|
|
CurrentDstField = newField;
|
|
|
|
try
|
|
{
|
|
if ( newField.IsInitOnly )
|
|
{
|
|
// We can't assign a new value to this field, so fall
|
|
// back to upgrading the new instance in-place
|
|
|
|
var newVal = newField.GetValue( null );
|
|
|
|
if ( newVal is null ) continue;
|
|
|
|
RootUpgraderGroup.TryUpgradeInstance( curVal, newVal );
|
|
continue;
|
|
}
|
|
|
|
var curValType = curVal.GetType();
|
|
|
|
if ( staticField == newField && !curValType.IsValueType && !IsSwappedType( curValType ) )
|
|
{
|
|
// We're processing a static field in a non-swapped type, and the value type isn't
|
|
// swapped either. Try upgrading in-place.
|
|
|
|
if ( RootUpgraderGroup.TryUpgradeInstance( curVal, curVal ) )
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Default to creating a new instance to upgrade the old one to
|
|
|
|
newField.SetValue( null, GetNewInstance( curVal ) );
|
|
}
|
|
#if !HOTLOAD_NOCATCH
|
|
catch ( Exception e )
|
|
{
|
|
Log( e, member: newField );
|
|
}
|
|
#endif
|
|
finally
|
|
{
|
|
CurrentPath = null;
|
|
CurrentSrcField = null;
|
|
CurrentDstField = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private object GetNewInstance( object oldInstance )
|
|
{
|
|
if ( oldInstance == null )
|
|
return null;
|
|
|
|
// Check for specific InstanceUpgraders for handling special types
|
|
if ( RootUpgraderGroup.TryCreateNewInstance( oldInstance, out var newInstance ) )
|
|
{
|
|
return newInstance;
|
|
}
|
|
|
|
// We shouldn't get here, the DefaultUpgrader will always be able to create new instances
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
private void ReportSwappedOutAssemblyReference( Type type, Assembly oldAsm )
|
|
{
|
|
Log( HotloadEntryType.Warning, $"Unable to find a type substitution without referencing a swapped-out assembly ({FormatAssemblyName( oldAsm )}).", type );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Make sure a candidate type substitution isn't at all defined in a swapped-out assembly.
|
|
/// This can go wrong if assembly B references assembly A, but only assembly A was swapped.
|
|
/// </summary>
|
|
private bool ValidateNewType( Type newType )
|
|
{
|
|
foreach ( var interfaceType in newType.GetInterfaces() )
|
|
{
|
|
if ( Swaps.ContainsKey( interfaceType.Assembly ) )
|
|
{
|
|
ReportSwappedOutAssemblyReference( newType, interfaceType.Assembly );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var baseType = newType;
|
|
|
|
while ( baseType != null )
|
|
{
|
|
if ( Swaps.ContainsKey( baseType.Assembly ) )
|
|
{
|
|
ReportSwappedOutAssemblyReference( newType, baseType.Assembly );
|
|
return false;
|
|
}
|
|
|
|
baseType = baseType.BaseType;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private Type GetSubstituteType( Assembly asm, Type oldType )
|
|
{
|
|
if ( asm == null )
|
|
{
|
|
// Assembly was unloaded in this swap
|
|
return null;
|
|
}
|
|
|
|
if ( GeneratedName.TryParse( oldType.Name, out var name ) )
|
|
{
|
|
switch ( name.Kind )
|
|
{
|
|
case GeneratedNameKind.LambdaDisplayClass:
|
|
case GeneratedNameKind.StateMachineType:
|
|
return GetNewNestedGeneratedType( oldType, name );
|
|
|
|
case GeneratedNameKind.ReadOnlyListType:
|
|
return GetNewReadOnlyListType( oldType );
|
|
|
|
case GeneratedNameKind.AnonymousType:
|
|
return GetNewAnonymousType( oldType );
|
|
|
|
default:
|
|
Log( HotloadEntryType.Error, $"Unhandled compiler-generated type", oldType );
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if ( oldType.FullName == null )
|
|
{
|
|
if ( oldType.DeclaringType == null ) return null;
|
|
var newDeclType = GetSubstituteType( asm, oldType.DeclaringType );
|
|
|
|
if ( oldType.IsGenericTypeParameter )
|
|
{
|
|
Assert.NotNull( oldType.DeclaringType );
|
|
|
|
return newDeclType?.GetGenericArguments()[oldType.GenericParameterPosition];
|
|
}
|
|
|
|
if ( oldType.IsGenericMethodParameter )
|
|
{
|
|
Assert.NotNull( oldType.DeclaringMethod );
|
|
|
|
var newMethod = GetNewInstance( oldType.DeclaringMethod ) as MethodBase;
|
|
|
|
return newMethod?.GetGenericArguments()[oldType.GenericParameterPosition];
|
|
}
|
|
|
|
return newDeclType?.GetNestedType( oldType.Name, BindingFlags.Public | BindingFlags.NonPublic );
|
|
}
|
|
|
|
var sub = asm.GetType( oldType.FullName );
|
|
|
|
if ( sub != null )
|
|
{
|
|
if ( !ValidateNewType( sub ) )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return sub;
|
|
}
|
|
|
|
// TODO
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// In a swapped assembly find a replacement type for this type.
|
|
/// Return null if no replacement is found.
|
|
/// </summary>
|
|
private Type GetNewType( Type oldType )
|
|
{
|
|
if ( SubstituteTypeCache.TryGetValue( oldType, out var newType ) ) return newType;
|
|
|
|
if ( IsSwappedType( oldType ) )
|
|
{
|
|
var typeInfo = oldType.GetTypeInfo();
|
|
var typeAssembly = typeInfo.Assembly;
|
|
|
|
if ( typeInfo.IsArray )
|
|
{
|
|
newType = GetNewArrayType( oldType );
|
|
}
|
|
else if ( typeInfo.IsByRef )
|
|
{
|
|
newType = GetNewByRefType( oldType );
|
|
}
|
|
else if ( typeInfo.IsConstructedGenericType )
|
|
{
|
|
newType = GetNewGenericType( oldType );
|
|
}
|
|
else
|
|
{
|
|
newType = Swaps.TryGetValue( typeAssembly, out var swapAssembly )
|
|
? GetSubstituteType( swapAssembly, oldType ) : oldType;
|
|
}
|
|
}
|
|
else if ( ValidateNewType( oldType ) )
|
|
{
|
|
newType = oldType;
|
|
}
|
|
else
|
|
{
|
|
newType = null;
|
|
}
|
|
|
|
SubstituteTypeCache.Add( oldType, newType );
|
|
return newType;
|
|
}
|
|
|
|
private Type GetNewArrayType( Type oldType )
|
|
{
|
|
var oldElemType = oldType.GetElementType();
|
|
var newElemType = GetNewType( oldElemType );
|
|
|
|
if ( newElemType == null )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if ( newElemType == oldElemType )
|
|
{
|
|
return oldType;
|
|
}
|
|
|
|
var rank = oldType.GetArrayRank();
|
|
|
|
return rank == 1 ? newElemType.MakeArrayType() : newElemType.MakeArrayType( rank );
|
|
}
|
|
|
|
private Type GetNewByRefType( Type oldType )
|
|
{
|
|
var oldElemType = oldType.GetElementType();
|
|
var newElemType = GetNewType( oldElemType );
|
|
|
|
if ( newElemType == null )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if ( newElemType == oldElemType )
|
|
{
|
|
return oldType;
|
|
}
|
|
|
|
return newElemType.MakeByRefType();
|
|
}
|
|
|
|
private Type GetNewGenericType( Type oldType )
|
|
{
|
|
var oldDefType = oldType.GetGenericTypeDefinition();
|
|
var newDefType = GetNewType( oldDefType );
|
|
|
|
if ( newDefType == null )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var madeSubstitution = newDefType != oldDefType;
|
|
|
|
var args = oldType.GetGenericArguments();
|
|
|
|
for ( var i = 0; i < args.Length; ++i )
|
|
{
|
|
var argSubType = GetNewType( args[i] );
|
|
|
|
if ( argSubType == null )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
madeSubstitution |= argSubType != args[i];
|
|
|
|
args[i] = argSubType;
|
|
}
|
|
|
|
return madeSubstitution ? newDefType.MakeGenericType( args ) : oldType;
|
|
}
|
|
|
|
private bool IsSwappedType( Type type )
|
|
{
|
|
if ( type == null )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( type.IsArray || type.IsByRef )
|
|
{
|
|
return IsSwappedType( type.GetElementType() );
|
|
}
|
|
|
|
if ( type.IsConstructedGenericType )
|
|
{
|
|
if ( IsSwappedType( type.GetGenericTypeDefinition() ) )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return type.GetGenericArguments().Any( IsSwappedType );
|
|
}
|
|
|
|
return Swaps.ContainsKey( type.Assembly );
|
|
}
|
|
|
|
private bool AreEqualTypes( Type a, Type b )
|
|
{
|
|
if ( a == b ) return true;
|
|
if ( a == null || b == null ) return false;
|
|
|
|
if ( a.IsArray && b.IsArray )
|
|
{
|
|
if ( a.GetArrayRank() != b.GetArrayRank() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return AreEqualTypes( a.GetElementType(), b.GetElementType() );
|
|
}
|
|
|
|
if ( a.IsByRef && b.IsByRef )
|
|
{
|
|
return AreEqualTypes( a.GetElementType(), b.GetElementType() );
|
|
}
|
|
|
|
if ( a.IsGenericType && b.IsGenericType )
|
|
{
|
|
if ( !AreEqualTypes( a.GetGenericTypeDefinition(), b.GetGenericTypeDefinition() ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var aArgs = a.GetGenericArguments();
|
|
var bArgs = b.GetGenericArguments();
|
|
|
|
if ( aArgs.Length != bArgs.Length )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for ( var i = 0; i < aArgs.Length; ++i )
|
|
{
|
|
if ( !AreEqualTypes( aArgs[i], bArgs[i] ) )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool AreEquivalentTypes( Type oldType, Type newType )
|
|
{
|
|
if ( oldType == newType ) return true;
|
|
if ( oldType == null || newType == null ) return false;
|
|
|
|
if ( oldType.IsArray != newType.IsArray ) return false;
|
|
if ( oldType.IsArray && newType.IsArray )
|
|
{
|
|
if ( oldType.GetArrayRank() != newType.GetArrayRank() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return AreEquivalentTypes( oldType.GetElementType(), newType.GetElementType() );
|
|
}
|
|
|
|
if ( oldType.IsByRef != newType.IsByRef ) return false;
|
|
if ( oldType.IsByRef && newType.IsByRef )
|
|
{
|
|
return AreEquivalentTypes( oldType.GetElementType(), newType.GetElementType() );
|
|
}
|
|
|
|
if ( oldType.IsConstructedGenericType != newType.IsConstructedGenericType ) return false;
|
|
if ( oldType.IsConstructedGenericType && newType.IsConstructedGenericType )
|
|
{
|
|
if ( !AreEquivalentTypes( oldType.GetGenericTypeDefinition(), newType.GetGenericTypeDefinition() ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var aArgs = oldType.GetGenericArguments();
|
|
var bArgs = newType.GetGenericArguments();
|
|
|
|
if ( aArgs.Length != bArgs.Length )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for ( var i = 0; i < aArgs.Length; ++i )
|
|
{
|
|
if ( !AreEquivalentTypes( aArgs[i], bArgs[i] ) )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if ( oldType.IsGenericMethodParameter != newType.IsGenericMethodParameter ) return false;
|
|
if ( oldType.IsGenericMethodParameter && newType.IsGenericMethodParameter )
|
|
{
|
|
// TODO: constraints
|
|
|
|
return oldType.GenericParameterPosition == newType.GenericParameterPosition;
|
|
}
|
|
|
|
if ( oldType.IsGenericTypeParameter != newType.IsGenericTypeParameter ) return false;
|
|
if ( oldType.IsGenericTypeParameter && newType.IsGenericTypeParameter )
|
|
{
|
|
// TODO: constraints
|
|
|
|
return oldType.GenericParameterPosition == newType.GenericParameterPosition;
|
|
}
|
|
|
|
var subType = GetNewType( oldType );
|
|
|
|
return subType != oldType && AreEqualTypes( subType, newType );
|
|
}
|
|
|
|
private void AddResultEntry( HotloadResultEntry entry )
|
|
{
|
|
CurrentResult?.AddEntry( entry );
|
|
|
|
if ( _logger == null )
|
|
{
|
|
return;
|
|
}
|
|
|
|
using var scope = SentrySdk.PushScope();
|
|
|
|
SentrySdk.ConfigureScope( x =>
|
|
{
|
|
x.Contexts["Source Member"] = new
|
|
{
|
|
FullName = entry.MemberString,
|
|
Path = entry.Path?.ToString()
|
|
};
|
|
} );
|
|
|
|
var details = entry.Message;
|
|
|
|
if ( entry.Member != null )
|
|
{
|
|
details = $"{details}\n Member: {entry.MemberString}";
|
|
}
|
|
|
|
if ( entry.Path != null )
|
|
{
|
|
details = $"{details}\n Path: {entry.Path}";
|
|
}
|
|
|
|
switch ( entry.Type )
|
|
{
|
|
case HotloadEntryType.Trace:
|
|
_logger.Trace( details );
|
|
break;
|
|
|
|
case HotloadEntryType.Information:
|
|
_logger.Info( details );
|
|
break;
|
|
|
|
case HotloadEntryType.Warning:
|
|
_logger.Warning( entry.Exception, details );
|
|
break;
|
|
|
|
case HotloadEntryType.Error:
|
|
_logger.Error( entry.Exception, details );
|
|
break;
|
|
}
|
|
}
|
|
|
|
internal void Log( HotloadEntryType type, FormattableString message, MemberInfo member = null )
|
|
{
|
|
AddResultEntry( new HotloadResultEntry( type, message, member, TracePaths ? CurrentPath : null ) );
|
|
}
|
|
|
|
internal void Log( Exception exception, FormattableString message = null, MemberInfo member = null )
|
|
{
|
|
AddResultEntry( new HotloadResultEntry( exception, message, member, TracePaths ? CurrentPath : null ) );
|
|
}
|
|
}
|
|
}
|