mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
474 lines
11 KiB
C#
474 lines
11 KiB
C#
using Sandbox.Diagnostics;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace Sandbox.Internal;
|
|
|
|
internal class EventSystem : IDisposable
|
|
{
|
|
Logger log = new Logger( "EventSystem" );
|
|
|
|
public EventSystem()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// A Type with events on it
|
|
/// </summary>
|
|
public class EventClass
|
|
{
|
|
public string Assembly;
|
|
public Type Type;
|
|
public List<EventAction> Events = new();
|
|
public List<object> Targets = new();
|
|
|
|
public void Destroy()
|
|
{
|
|
Assembly = null;
|
|
Type = null;
|
|
Targets.Clear();
|
|
|
|
foreach ( var e in Events.ToArray() )
|
|
{
|
|
e.Destroy();
|
|
}
|
|
|
|
Assert.AreEqual( 0, Events.Count() );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A method on a type
|
|
/// </summary>
|
|
public class EventAction : IComparable<EventAction>
|
|
{
|
|
public int Priority;
|
|
public EventClass Class;
|
|
public EventList Group;
|
|
public EventDelegate Delegate;
|
|
public bool IsStatic;
|
|
|
|
public int CompareTo( EventAction other )
|
|
{
|
|
return other.Priority.CompareTo( Priority );
|
|
}
|
|
|
|
public void Destroy()
|
|
{
|
|
Class.Events.Remove( this );
|
|
Group.Remove( this );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run this event action, aggregating any exceptions.
|
|
/// </summary>
|
|
internal void Run( string name, object[] args = null )
|
|
{
|
|
if ( IsStatic )
|
|
{
|
|
try
|
|
{
|
|
Delegate( null, args );
|
|
return;
|
|
}
|
|
catch ( Exception ex )
|
|
{
|
|
throw new TargetInvocationException( $"Error calling event '{name}' on '{Class.Type}'", ex.InnerException ?? ex );
|
|
}
|
|
}
|
|
|
|
List<Exception> innerExceptions = null;
|
|
|
|
for ( int i = Class.Targets.Count - 1; i >= 0; i-- )
|
|
{
|
|
var target = Class.Targets[i];
|
|
|
|
try
|
|
{
|
|
Delegate( target, args );
|
|
}
|
|
catch ( Exception ex )
|
|
{
|
|
innerExceptions ??= new();
|
|
innerExceptions.Add( new TargetInvocationException( $"Error calling event '{name}' on '{target}'", ex.InnerException ?? ex ) );
|
|
}
|
|
}
|
|
|
|
switch ( innerExceptions )
|
|
{
|
|
case { Count: 1 }:
|
|
throw innerExceptions[0];
|
|
case { Count: > 1 }:
|
|
throw new AggregateException( innerExceptions.ToArray() );
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A list of events, usually indexed by the event name
|
|
/// </summary>
|
|
public class EventList : List<EventAction>
|
|
{
|
|
/// <summary>
|
|
/// Run this event list, aggregating any exceptions.
|
|
/// </summary>
|
|
internal void Run( string name, object[] args = null )
|
|
{
|
|
List<Exception> innerExceptions = null;
|
|
|
|
for ( int i = Count - 1; i >= 0; i-- )
|
|
{
|
|
var action = this[i];
|
|
|
|
try
|
|
{
|
|
action.Run( name, args );
|
|
}
|
|
catch ( Exception ex )
|
|
{
|
|
innerExceptions ??= new();
|
|
innerExceptions.Add( new TargetInvocationException( $"Error calling event '{name}' on '{action.Class.Type}'", ex.InnerException ?? ex ) );
|
|
}
|
|
}
|
|
|
|
switch ( innerExceptions )
|
|
{
|
|
case { Count: 1 }:
|
|
throw innerExceptions[0];
|
|
case { Count: > 1 }:
|
|
throw new AggregateException( innerExceptions.ToArray() );
|
|
}
|
|
}
|
|
}
|
|
|
|
public delegate void EventDelegate( object root, object[] parms );
|
|
|
|
Dictionary<string, EventClass> Classes = new();
|
|
Dictionary<string, EventList> Groups = new( StringComparer.OrdinalIgnoreCase );
|
|
|
|
/// <summary>
|
|
/// Instances that have had their assembly removed. We keep them around becuase the
|
|
/// assembly might be re-registered.
|
|
/// </summary>
|
|
HashSet<object> OrphanedInstances = new();
|
|
|
|
string TypeKey( Type t )
|
|
{
|
|
return $"{t.Assembly.GetName().Name}/{t.FullName}";
|
|
}
|
|
|
|
void AddEventsForType( Type t, Type rootType = null )
|
|
{
|
|
EventClass classEvent = null;
|
|
|
|
foreach ( var m in t.GetMethods( BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ) )
|
|
{
|
|
if ( rootType != null && !m.IsPrivate ) continue;
|
|
|
|
foreach ( var attr in m.GetCustomAttributes<EventAttribute>( true ) )
|
|
{
|
|
if ( classEvent == null )
|
|
{
|
|
var typeKey = TypeKey( rootType ?? t );
|
|
if ( !Classes.TryGetValue( typeKey, out classEvent ) )
|
|
{
|
|
classEvent = new EventClass { Assembly = (rootType ?? t).Assembly.GetName().Name, Type = rootType ?? t };
|
|
Classes[typeKey] = classEvent;
|
|
}
|
|
}
|
|
|
|
log.Trace( $" {t} -> {m} - {attr}" );
|
|
|
|
var group = GetGroup( attr.EventName, true );
|
|
|
|
var eventAction = new EventAction
|
|
{
|
|
Priority = attr.Priority,
|
|
Class = classEvent,
|
|
Group = group,
|
|
Delegate = BuildDelegate( m ),
|
|
IsStatic = m.IsStatic
|
|
};
|
|
|
|
|
|
classEvent.Events.Add( eventAction );
|
|
eventAction.Group.Add( eventAction );
|
|
|
|
group.Sort();
|
|
}
|
|
}
|
|
|
|
if ( t.BaseType is not null && t.BaseType != typeof( object ) )
|
|
AddEventsForType( t.BaseType, rootType ?? t );
|
|
}
|
|
|
|
EventDelegate BuildDelegate( MethodInfo info )
|
|
{
|
|
// TODO - handle arguments
|
|
|
|
var parameters = info.GetParameters();
|
|
object[] args = default;
|
|
if ( parameters.Length > 0 )
|
|
{
|
|
args = new object[parameters.Length];
|
|
}
|
|
|
|
EventDelegate d = ( o, p ) =>
|
|
{
|
|
if ( args != null )
|
|
{
|
|
if ( p == null )
|
|
throw new ArgumentException( $"{info.Name} expects {args.Length} arguments but event passed none" );
|
|
|
|
if ( p.Length != args.Length )
|
|
throw new ArgumentException( $"Event passed {p.Length} arguments but {info.Name} expects {args.Length}" );
|
|
|
|
for ( int i = 0; i < args.Length; i++ )
|
|
{
|
|
args[i] = p[i];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( p != null && p.Length != 0 )
|
|
throw new ArgumentException( $"Event passed {p.Length} arguments but {info.Name} expects none" );
|
|
}
|
|
|
|
info?.Invoke( o, args );
|
|
};
|
|
|
|
return d;
|
|
}
|
|
|
|
internal EventList GetGroup( string name, bool create )
|
|
{
|
|
name = name.ToLower(); // TODO - hotload bug makes dict case senstitive
|
|
|
|
if ( Groups.TryGetValue( name, out var group ) )
|
|
return group;
|
|
|
|
if ( !create )
|
|
return null;
|
|
|
|
group = new EventList();
|
|
Groups.Add( name, group );
|
|
return group;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register an assembly. If old assembly is valid, we try to remove all of the old event hooks
|
|
/// from this assembly, while retaining a list of objects.
|
|
/// </summary>
|
|
internal void UnregisterAssembly( Assembly assm )
|
|
{
|
|
log.Trace( $"Removing {assm}" );
|
|
|
|
// collect instances
|
|
string assemblyName = assm.GetName().Name;
|
|
Dictionary<string, EventClass> classes = Classes.Where( x => x.Value.Assembly == assemblyName ).ToDictionary( x => x.Key, x => x.Value );
|
|
|
|
log.Trace( $"Found {classes.Count()} Types" );
|
|
|
|
object[] instances = classes.SelectMany( x => x.Value.Targets ).ToArray();
|
|
|
|
foreach ( var instance in instances )
|
|
{
|
|
OrphanedInstances.Add( instance );
|
|
}
|
|
|
|
log.Trace( $"Found {instances.Count()} Instances" );
|
|
|
|
foreach ( var clss in classes )
|
|
{
|
|
clss.Value.Destroy();
|
|
Classes.Remove( clss.Key );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register an assembly. If old assembly is valid, we try to remove all of the old event hooks
|
|
/// from this assembly, while retaining a list of objects.
|
|
/// </summary>
|
|
internal void RegisterAssembly( Assembly assm )
|
|
{
|
|
log.Trace( $"Registering Event System: [{assm}]" );
|
|
|
|
if ( assm == null )
|
|
return;
|
|
|
|
var assemblyName = assm.GetName().Name;
|
|
EventClass[] classes = Classes.Values.Where( x => x.Assembly == assemblyName ).ToArray();
|
|
|
|
Assert.AreEqual( 0, classes.Length, "Register Assembly - event already contains this dll!" );
|
|
|
|
var newTypes = assm.GetTypes();
|
|
|
|
foreach ( var type in newTypes )
|
|
{
|
|
Classes.TryGetValue( TypeKey( type ), out EventClass classEvent );
|
|
Assert.IsNull( classEvent );
|
|
|
|
AddEventsForType( type );
|
|
}
|
|
|
|
foreach ( var instance in OrphanedInstances.ToArray() )
|
|
{
|
|
// instance could have turned null because hotload lost the type
|
|
if ( instance is null )
|
|
{
|
|
OrphanedInstances.Remove( instance );
|
|
return;
|
|
}
|
|
|
|
if ( instance.GetType().Assembly == assm )
|
|
{
|
|
log.Trace( $"Re-registering orphan: {instance}" );
|
|
Register( instance );
|
|
OrphanedInstances.Remove( instance );
|
|
continue;
|
|
}
|
|
|
|
// If the assembly didn't match - then if the assembly has the same name
|
|
// it could be a sign of trouble - because all the instances should have
|
|
// been swapped to the new assembly type
|
|
if ( instance.GetType().Assembly.GetName().Name == assemblyName )
|
|
{
|
|
log.Warning( $"instance {instance} is from {instance.GetType().Assembly} - but doesn't match {assm} - should this have been hotload swapped?" );
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void Run( string v )
|
|
{
|
|
try
|
|
{
|
|
GetGroup( v, false )?.Run( v );
|
|
}
|
|
catch ( Exception ex )
|
|
{
|
|
log.Error( ex );
|
|
}
|
|
}
|
|
|
|
internal void Run( string v, params object[] list )
|
|
{
|
|
try
|
|
{
|
|
GetGroup( v, false )?.Run( v, list );
|
|
}
|
|
catch ( Exception ex )
|
|
{
|
|
log.Error( ex );
|
|
}
|
|
}
|
|
|
|
WeakHashSet<object> AllTargets = new WeakHashSet<object>();
|
|
|
|
internal void RunInterface<T>( Action<T> t )
|
|
{
|
|
foreach ( var e in AllTargets.OfType<T>().ToArray() )
|
|
{
|
|
t.Invoke( e );
|
|
}
|
|
}
|
|
|
|
internal void Register( object obj )
|
|
{
|
|
AllTargets.Add( obj );
|
|
|
|
if ( !Classes.TryGetValue( TypeKey( obj.GetType() ), out var type ) )
|
|
return;
|
|
|
|
type.Targets.Add( obj );
|
|
}
|
|
|
|
internal void Unregister( object obj )
|
|
{
|
|
AllTargets.Remove( obj );
|
|
|
|
if ( OrphanedInstances.Remove( obj ) )
|
|
return;
|
|
|
|
if ( !Classes.TryGetValue( TypeKey( obj.GetType() ), out var type ) )
|
|
return;
|
|
|
|
type.Targets.Remove( obj );
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Classes.Clear();
|
|
Classes = null;
|
|
|
|
Groups.Clear();
|
|
Groups = null;
|
|
}
|
|
}
|
|
|
|
#nullable enable
|
|
|
|
internal sealed class WeakHashSet<T> : IEnumerable<T>
|
|
where T : class
|
|
{
|
|
// TODO: We're only accessing items in this with OfType<T2>, should we index by type?
|
|
// TODO: T is always object, can we simplify?
|
|
|
|
private readonly ConditionalWeakTable<T, object?> _weakTable = new();
|
|
|
|
private int _lastGcCount;
|
|
|
|
public void Add( T item )
|
|
{
|
|
ClearCaches();
|
|
_weakTable.AddOrUpdate( item, null );
|
|
}
|
|
|
|
public bool Contains( T item ) => _weakTable.TryGetValue( item, out _ );
|
|
|
|
public bool Remove( T item )
|
|
{
|
|
ClearCaches();
|
|
return _weakTable.Remove( item );
|
|
}
|
|
|
|
#region IEnumerable
|
|
|
|
private readonly object _lock = new();
|
|
private WeakReference<IEnumerable<T>>? _liveItemCache;
|
|
|
|
private void ClearCaches() => _liveItemCache = null;
|
|
|
|
public IEnumerator<T> GetEnumerator()
|
|
{
|
|
// Enumerating using _weakTable.Select( x => x.Key ) is quite slow, so we cache it.
|
|
|
|
// The cache is invalidated:
|
|
// 1) Explicitly when items are added / removed
|
|
// 2) Implicitly when a GC happens
|
|
|
|
var gcCount = GC.CollectionCount( 0 );
|
|
|
|
if ( _lastGcCount == gcCount && _liveItemCache?.TryGetTarget( out var items ) is true )
|
|
{
|
|
return items.GetEnumerator();
|
|
}
|
|
|
|
lock ( _lock )
|
|
{
|
|
_lastGcCount = gcCount;
|
|
_liveItemCache = new WeakReference<IEnumerable<T>>( items = _weakTable.Select( x => x.Key ).ToArray() );
|
|
}
|
|
|
|
return items.GetEnumerator();
|
|
}
|
|
|
|
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
|
|
|
#endregion
|
|
}
|