Files
sbox-public/engine/Sandbox.Hotload/InstanceUpgrader.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

332 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Reflection;
using Mono.Cecil;
using Sandbox.Upgraders;
namespace Sandbox
{
public partial class Hotload
{
/// <summary>
/// Interface to implement a custom object instance upgrade process for types that match a condition.
/// Instances of any derived types will be created and added to a <see cref="Sandbox.Hotload"/> instance that uses
/// <see cref="AddUpgraders"/> on the declaring assembly of the derived type, unless a
/// <see cref="DisableAutoCreationAttribute"/> has been specified.
///
/// You can configure which order <see cref="IInstanceUpgrader"/>s are queried by using <see cref="UpgraderGroupAttribute"/>,
/// <see cref="AttemptBeforeAttribute"/> and / or <see cref="AttemptAfterAttribute"/>.
/// </summary>
public interface IInstanceUpgrader
{
bool IsInitialized { get; }
void Initialize( Hotload hotload );
void HotloadStart();
void HotloadComplete();
void ClearCache();
bool ShouldProcessType( Type type );
bool TryCreateNewInstance( object oldInstance, out object newInstance );
bool TryUpgradeInstance( object oldInstance, object newInstance );
}
public interface IInstanceProcessor
{
int ProcessInstance( object oldInstance, object newInstance );
}
#pragma warning disable CA1034 // Nested types should not be visible
public abstract partial class InstanceUpgrader : IInstanceUpgrader, IInstanceProcessor
#pragma warning restore CA1034 // Nested types should not be visible
{
private Hotload Hotload;
protected DefaultUpgrader DefaultUpgrader { get; private set; }
protected CachedUpgrader CachedUpgrader { get; private set; }
public bool IsInitialized => Hotload != null;
public bool TracePaths => Hotload.TracePaths;
public ReferencePath CurrentPath
{
get => Hotload.CurrentPath;
set => Hotload.CurrentPath = value;
}
public FieldInfo CurrentSrcField
{
get => Hotload.CurrentSrcField;
set => Hotload.CurrentSrcField = value;
}
public FieldInfo CurrentDstField
{
get => Hotload.CurrentDstField;
set => Hotload.CurrentDstField = value;
}
void IInstanceUpgrader.Initialize( Hotload hotload )
{
Hotload = hotload;
DefaultUpgrader = hotload.GetUpgrader<DefaultUpgrader>();
CachedUpgrader = hotload.GetUpgrader<CachedUpgrader>();
OnInitialize();
}
void IInstanceUpgrader.HotloadStart()
{
OnHotloadStart();
}
void IInstanceUpgrader.HotloadComplete()
{
OnHotloadComplete();
}
void IInstanceUpgrader.ClearCache()
{
OnClearCache();
}
/// <summary>
/// A mapping of assembles to swap with new versions.
/// </summary>
protected IReadOnlyDictionary<Assembly, Assembly> Swaps => Hotload.Swaps;
protected TUpgrader GetUpgrader<TUpgrader>()
where TUpgrader : IInstanceUpgrader
{
return Hotload.GetUpgrader<TUpgrader>();
}
protected TUpgrader GetUpgraderOrDefault<TUpgrader>()
where TUpgrader : IInstanceUpgrader
{
return Hotload.TryGetUpgrader<TUpgrader>( out var upgrader ) ? upgrader : default;
}
protected bool TryGetUpgrader<TUpgrader>( out TUpgrader upgrader )
where TUpgrader : IInstanceUpgrader
{
return Hotload.TryGetUpgrader( out upgrader );
}
protected bool IsAssemblyIgnored( Assembly asm ) => Hotload.IsAssemblyIgnored( asm );
/// <summary>
/// When hotswapping this will switch types from the old assembly into the type from the new assembly.
/// </summary>
/// <param name="oldType">The old type.</param>
/// <returns>The new type, or null if no substitution exists. The old type will be returned if it's still valid (not from a swapped assembly).</returns>
protected Type GetNewType( Type oldType ) => Hotload.GetNewType( oldType );
/// <summary>
/// Returns an upgraded version of the given object, replacing any types from a swapped-out
/// assembly with their new up-to-date types. The result is cached, so if you pass the same
/// object to this method multiple times it will always return the same instance. Fields inside
/// the new instance may not be initialized until later in the hotload.
/// </summary>
/// <param name="oldInstance">Object to upgrade.</param>
/// <returns>An upgraded version of the given object.</returns>
protected object GetNewInstance( object oldInstance ) => Hotload.GetNewInstance( oldInstance );
protected T GetNewInstance<T>( T oldInstance ) => (T)GetNewInstance( (object)oldInstance );
protected void AddCachedInstance( object oldInstance, object newInstance )
{
CachedUpgrader.AddCachedInstance( oldInstance, newInstance );
}
protected void SuppressFinalize( object oldInstance, object newInstance )
{
if ( !ReferenceEquals( oldInstance, newInstance ) )
{
GC.SuppressFinalize( oldInstance );
}
}
protected bool TryGetDefaultValue( FieldInfo field, out object value ) =>
Hotload.TryGetDefaultValue( field, out value );
protected bool IsSwappedType( Type type ) => Hotload.IsSwappedType( type );
protected bool AreEquivalentTypes( Type oldType, Type newType ) =>
Hotload.AreEquivalentTypes( oldType, newType );
protected bool AreEqualTypes( Type a, Type b ) =>
Hotload.AreEqualTypes( a, b );
protected MethodBase FindScopeMethod( Type declaringType, int scopeMethodOrdinal ) =>
Hotload.FindScopeMethod( declaringType, null, scopeMethodOrdinal );
protected MethodBase FindScopeMethod( Type declaringType, string scopeMethodName, int scopeMethodOrdinal ) =>
Hotload.FindScopeMethod( declaringType, scopeMethodName, scopeMethodOrdinal );
protected int GetScopeMethodOrdinal( MethodBase scopeMethod ) =>
Hotload.GetScopeMethodOrdinal( scopeMethod );
/// <summary>
/// Logs a message in the current hotload.
/// </summary>
protected void Log( HotloadEntryType type, FormattableString message, MemberInfo member = null ) =>
Hotload.Log( type, message, member );
/// <summary>
/// Logs an exception in the current hotload.
/// </summary>
protected void Log( Exception exception, FormattableString message = null, MemberInfo member = null ) =>
Hotload.Log( exception, message, member );
/// <summary>
/// Called when this upgrader has been added to a <see cref="Hotload"/> instance.
/// </summary>
protected virtual void OnInitialize() { }
protected virtual void OnHotloadStart() { }
protected virtual void OnHotloadComplete() { }
/// <summary>
/// Called between hotloads, should clear up any cached resources that won't be needed in future hotloads.
/// </summary>
protected virtual void OnClearCache() { }
/// <summary>
/// Check to see if this upgrader can possibly handle the given type.
/// </summary>
/// <param name="type">Type to upgrade an instance of.</param>
/// <returns>True if this upgrader should attempt to upgrade an instance of the given type.</returns>
public abstract bool ShouldProcessType( Type type );
public bool TryCreateNewInstance( object oldInstance, out object newInstance )
{
if ( !OnTryCreateNewInstance( oldInstance, out newInstance ) )
{
return false;
}
if ( !OnTryUpgradeInstance( oldInstance, newInstance, false ) )
{
return true;
}
return true;
}
public bool TryUpgradeInstance( object oldInstance, object newInstance )
{
if ( !OnTryUpgradeInstance( oldInstance, newInstance, true ) )
{
return false;
}
return true;
}
protected void ScheduleProcessInstance( object oldInstance, object newInstance )
{
Hotload.ScheduleInstanceTask( this, oldInstance, newInstance );
}
protected void ScheduleLateProcessInstance( object oldInstance, object newInstance )
{
Hotload.ScheduleLateInstanceTask( this, oldInstance, newInstance );
}
protected AssemblyDefinition GetAssemblyDefinition( Assembly asm ) => Hotload.GetAssemblyDefinition( asm );
protected TypeDefinition GetTypeDefinition( Type type ) => Hotload.GetTypeDefinition( type );
protected MethodDefinition GetMethodDefinition( MethodBase method ) => Hotload.GetMethodDefinition( method );
/// <summary>
/// If this upgrader supports upgrading the given <paramref name="oldInstance"/>, returns <value>true</value> and
/// assigns <paramref name="newInstance"/> to be the value that should replace <paramref name="oldInstance"/>. This
/// method doesn't need to copy the inner state of the instance across, but just creates an empty instance to be
/// populated later.
/// </summary>
/// <remarks>
/// <para>
/// It's safe to just directly assign <paramref name="newInstance"/> to <paramref name="oldInstance"/> if the type
/// isn't declared in a replaced assembly.
/// </para>
/// <para>
/// Returning true will cause <see cref="OnTryUpgradeInstance"/> to be called immediately after this method, which
/// schedules copying the state of the old instance to the new one.
/// </para>
/// </remarks>
/// <param name="oldInstance">Instance that should be replaced / upgraded.</param>
/// <param name="newInstance">
/// If this method returns true, this should contain the instance that replaces <paramref name="oldInstance"/>,
/// or <paramref name="oldInstance"/> itself if no replacement is necessary.
/// </param>
/// <returns>True if this upgrader handles the replacement of the given <paramref name="oldInstance"/>.</returns>
protected abstract bool OnTryCreateNewInstance( object oldInstance, out object newInstance );
/// <summary>
/// Called immediately after <see cref="OnTryCreateNewInstance"/> if it returned true, or on instances from fields
/// that can't be re-assigned (see <see cref="FieldInfo.IsInitOnly"/>). This method determines what kind of extra
/// processing is required for the given replacement.
/// </summary>
/// <remarks>
/// <para>
/// In this method we can call things like <see cref="ProcessInstance"/>, <see cref="ScheduleInstanceTask"/> or
/// <see cref="ScheduleLateInstanceTask"/> to handle copying values from the old instance to the new one.
/// </para>
/// <para>
/// If <paramref name="newInstance"/> should be cached as the canonical replacement for <paramref name="oldInstance"/>,
/// call <see cref="AddCachedInstance"/> here.
/// </para>
/// <para>
/// If finalization should be suppressed, call <see cref="SuppressFinalize"/>.
/// </para>
/// </remarks>
/// <param name="oldInstance">Original instance that is being replaced / upgraded from.</param>
/// <param name="newInstance">
/// New instance that replaces <paramref name="oldInstance"/>, or <paramref name="oldInstance"/> itself if no replacement is necessary.
/// </param>
/// <param name="createdElsewhere">
/// True if <paramref name="newInstance"/> was created outside of the hotloading system, for example when the
/// containing field has <see cref="FieldInfo.IsInitOnly"/> set to true. Otherwise, when false, <see cref="OnTryCreateNewInstance"/>
/// will have been called just before this method.
/// </param>
/// <returns></returns>
protected abstract bool OnTryUpgradeInstance( object oldInstance, object newInstance, bool createdElsewhere );
public int ProcessInstance( object oldInstance, object newInstance )
{
try
{
return OnProcessInstance( oldInstance, newInstance );
}
#if !HOTLOAD_NOCATCH
catch ( Exception e )
{
Log( e, member: oldInstance.GetType() );
return 1;
}
#endif
finally
{
}
}
/// <summary>
/// Perform extra field processing on a new instance that has previously been created by this upgrader in
/// <see cref="OnTryCreateNewInstance"/>. This is a good place to discover any other instances that should be upgraded
/// that are stored in <paramref name="oldInstance"/>, which can be upgraded by calling <see cref="GetNewInstance"/>.
/// </summary>
/// <param name="oldInstance">The original instance that was upgraded.</param>
/// <param name="newInstance">Upgraded version of <paramref name="oldInstance"/>, or even the same object if no upgrade
/// was required.</param>
/// <returns>Roughly how many instances were processed by this method. Only used for performance stats.</returns>
protected virtual int OnProcessInstance( object oldInstance, object newInstance )
{
return 1;
}
}
}
}