mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-05-29 09:15:27 -04:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
384 lines
8.4 KiB
C#
384 lines
8.4 KiB
C#
using System.Collections;
|
|
using Sandbox.MovieMaker;
|
|
using System.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
|
|
namespace Editor.MovieMaker;
|
|
|
|
#nullable enable
|
|
public static class CollectionExtensions
|
|
{
|
|
public static T? FirstOrNull<T>( this IEnumerable<T> enumerable, Func<T, bool> predicate )
|
|
where T : struct
|
|
{
|
|
foreach ( var item in enumerable )
|
|
{
|
|
if ( predicate( item ) )
|
|
{
|
|
return item;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static IReadOnlyList<T> Slice<T>( this IReadOnlyList<T> list, int offset, int count )
|
|
{
|
|
if ( offset < 0 )
|
|
{
|
|
throw new ArgumentException( "Offset must be >= 0.", nameof( offset ) );
|
|
}
|
|
|
|
if ( list.Count < offset + count )
|
|
{
|
|
throw new ArgumentException( "Slice exceeds list element count.", nameof( count ) );
|
|
}
|
|
|
|
// Fast paths
|
|
|
|
if ( count == 0 ) return Array.Empty<T>();
|
|
if ( offset == 0 && count == list.Count ) return list;
|
|
|
|
switch ( list )
|
|
{
|
|
case T[] array:
|
|
return new ArraySegment<T>( array, offset, count );
|
|
case ImmutableArray<T> immutableArray:
|
|
return immutableArray.Slice( offset, count );
|
|
case ArraySegment<T> segment:
|
|
return segment.Slice( offset, count );
|
|
}
|
|
|
|
// Slow copy
|
|
|
|
return list.Skip( offset ).Take( count ).ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given two ascending lists of time ranges, union any overlapping pairs between the two lists and return them.
|
|
/// </summary>
|
|
public static IEnumerable<MovieTimeRange> Union( this IEnumerable<MovieTimeRange> first, IEnumerable<MovieTimeRange> second )
|
|
{
|
|
using var enumeratorA = first.GetEnumerator();
|
|
using var enumeratorB = second.GetEnumerator();
|
|
|
|
var hasItemA = enumeratorA.MoveNext();
|
|
var hasItemB = enumeratorB.MoveNext();
|
|
|
|
while ( hasItemA || hasItemB )
|
|
{
|
|
var next = !hasItemB || hasItemA && enumeratorA.Current.Start <= enumeratorB.Current.Start
|
|
? enumeratorA.Current
|
|
: enumeratorB.Current;
|
|
|
|
while ( true )
|
|
{
|
|
if ( hasItemA && next.Intersect( enumeratorA.Current ) is not null )
|
|
{
|
|
next = next.Union( enumeratorA.Current );
|
|
hasItemA = enumeratorA.MoveNext();
|
|
continue;
|
|
}
|
|
|
|
if ( hasItemB && next.Intersect( enumeratorB.Current ) is not null )
|
|
{
|
|
next = next.Union( enumeratorB.Current );
|
|
hasItemB = enumeratorB.MoveNext();
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
yield return next;
|
|
}
|
|
}
|
|
|
|
public static T Pop<T>( this List<T> list )
|
|
{
|
|
var item = list[^1];
|
|
list.RemoveAt( list.Count - 1 );
|
|
|
|
return item;
|
|
}
|
|
}
|
|
|
|
public interface ISynchronizedList<TSrc, TItem> : IReadOnlyList<TItem>
|
|
{
|
|
public void Clear() => Update( [] );
|
|
bool Update( IEnumerable<TSrc> source );
|
|
int IndexOf( TSrc src );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maintains a set of <typeparamref name="TItem"/>, mapped from a collection of <typeparamref name="TSrc"/>.
|
|
/// </summary>
|
|
public sealed class SynchronizedSet<TSrc, TItem> : ISynchronizedList<TSrc, TItem>, IHotloadManaged
|
|
where TSrc : notnull
|
|
{
|
|
[SuppressNullKeyWarning]
|
|
private readonly Dictionary<TSrc, TItem> _items;
|
|
|
|
[SuppressNullKeyWarning]
|
|
private readonly Dictionary<TSrc, int> _indices;
|
|
|
|
private readonly List<KeyValuePair<TSrc, TItem>> _ordered = new();
|
|
|
|
private readonly Func<TSrc, TItem> _addFunc;
|
|
private readonly Action<TItem>? _removeAction;
|
|
private readonly Func<TSrc, TItem, bool>? _updateAction;
|
|
|
|
private bool _keysCanBeInvalid;
|
|
private bool _keysWereInvalid;
|
|
|
|
public SynchronizedSet( Func<TSrc, TItem> addFunc,
|
|
Action<TItem>? removeAction = null,
|
|
Func<TSrc, TItem, bool>? updateAction = null,
|
|
IEqualityComparer<TSrc>? comparer = null )
|
|
{
|
|
_addFunc = addFunc;
|
|
_removeAction = removeAction;
|
|
_updateAction = updateAction;
|
|
|
|
_items = new Dictionary<TSrc, TItem>( comparer );
|
|
_indices = new Dictionary<TSrc, int>( comparer );
|
|
}
|
|
|
|
public TItem? Find( TSrc key )
|
|
{
|
|
RemoveInvalidKeys();
|
|
return _items.GetValueOrDefault( key );
|
|
}
|
|
|
|
public void Clear() => Update( [] );
|
|
|
|
public bool Update( IEnumerable<TSrc> source )
|
|
{
|
|
RemoveInvalidKeys();
|
|
|
|
_indices.Clear();
|
|
|
|
foreach ( var src in source )
|
|
{
|
|
_indices.Add( src, _indices.Count );
|
|
}
|
|
|
|
// We might have removed some items after a hotload, so
|
|
// that should count as the collection being changed here
|
|
|
|
var changed = _keysWereInvalid;
|
|
|
|
_keysWereInvalid = false;
|
|
|
|
// Remove items
|
|
|
|
for ( var i = _ordered.Count - 1; i >= 0; --i )
|
|
{
|
|
var src = _ordered[i].Key;
|
|
|
|
if ( _indices.ContainsKey( src ) ) continue;
|
|
|
|
_ordered.RemoveAt( i );
|
|
|
|
if ( !_items.Remove( src, out var item ) ) continue;
|
|
|
|
_removeAction?.Invoke( item );
|
|
|
|
changed = true;
|
|
}
|
|
|
|
// Add items
|
|
|
|
foreach ( var src in _indices.Keys )
|
|
{
|
|
if ( _items.ContainsKey( src ) ) continue;
|
|
|
|
var item = _addFunc( src );
|
|
|
|
_items.Add( src, item );
|
|
_ordered.Add( new KeyValuePair<TSrc, TItem>( src, item ) );
|
|
|
|
changed = true;
|
|
}
|
|
|
|
// Sort and update items
|
|
|
|
_ordered.Sort( ( a, b ) => _indices[a.Key] - _indices[b.Key] );
|
|
|
|
if ( _updateAction is { } updateAction )
|
|
{
|
|
foreach ( var pair in _ordered )
|
|
{
|
|
changed |= updateAction( pair.Key, pair.Value );
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
public int IndexOf( TSrc src )
|
|
{
|
|
RemoveInvalidKeys();
|
|
return _indices.TryGetValue( src, out var index ) ? index : -1;
|
|
}
|
|
|
|
public IEnumerator<TItem> GetEnumerator()
|
|
{
|
|
RemoveInvalidKeys();
|
|
return _ordered.Select( x => x.Value ).GetEnumerator();
|
|
}
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
{
|
|
RemoveInvalidKeys();
|
|
return _items.GetEnumerator();
|
|
}
|
|
|
|
public int Count
|
|
{
|
|
get
|
|
{
|
|
RemoveInvalidKeys();
|
|
return _items.Count;
|
|
}
|
|
}
|
|
|
|
public TItem this[ int index ]
|
|
{
|
|
get
|
|
{
|
|
RemoveInvalidKeys();
|
|
return _ordered[index].Value;
|
|
}
|
|
}
|
|
|
|
void IHotloadManaged.Persisted() => _keysCanBeInvalid = true;
|
|
void IHotloadManaged.Created( IReadOnlyDictionary<string, object?> state ) => _keysCanBeInvalid = true;
|
|
|
|
/// <summary>
|
|
/// Called after hotload to remove any items that didn't survive because their types got removed.
|
|
/// </summary>
|
|
private void RemoveInvalidKeys()
|
|
{
|
|
if ( !_keysCanBeInvalid ) return;
|
|
|
|
_keysCanBeInvalid = false;
|
|
|
|
// First pass: remove invalid / null items
|
|
|
|
for ( var i = _ordered.Count - 1; i >= 0; i-- )
|
|
{
|
|
var (src, dst) = _ordered[i];
|
|
|
|
if ( (TSrc?)src is null )
|
|
{
|
|
// Null items will have been removed from _items dict during hotload,
|
|
// because dictionaries can't have null keys
|
|
|
|
_removeAction?.Invoke( dst );
|
|
_ordered.RemoveAt( i );
|
|
|
|
_keysWereInvalid = true;
|
|
}
|
|
else if ( src is IValid { IsValid: false } )
|
|
{
|
|
_removeAction?.Invoke( dst );
|
|
_ordered.RemoveAt( i );
|
|
_items.Remove( src );
|
|
|
|
_keysWereInvalid = true;
|
|
}
|
|
}
|
|
|
|
// Second pass: update indices dict
|
|
|
|
_indices.Clear();
|
|
|
|
for ( var i = 0; i < _ordered.Count; i++ )
|
|
{
|
|
_indices.Add( _ordered[i].Key, i );
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maintains a list of <typeparamref name="TItem"/> with the same length as a list of <typeparamref name="TSrc"/>.
|
|
/// </summary>
|
|
public sealed class SynchronizedList<TSrc, TItem> : ISynchronizedList<TSrc, TItem>
|
|
{
|
|
private readonly List<TSrc> _sources = new();
|
|
private readonly List<TItem> _items = new();
|
|
|
|
private readonly Func<TSrc, TItem> _addFunc;
|
|
private readonly Action<TItem>? _removeAction;
|
|
private readonly UpdateItemDelegate? _updateAction;
|
|
|
|
public IEnumerable<TSrc> Sources => _sources;
|
|
|
|
public delegate bool UpdateItemDelegate( TSrc source, ref TItem item );
|
|
|
|
public SynchronizedList( Func<TSrc, TItem> addFunc,
|
|
Action<TItem>? removeAction = null,
|
|
UpdateItemDelegate? updateAction = null )
|
|
{
|
|
_addFunc = addFunc;
|
|
_removeAction = removeAction;
|
|
_updateAction = updateAction;
|
|
}
|
|
|
|
public void Clear() => Update( [] );
|
|
|
|
public bool Update( IEnumerable<TSrc> source )
|
|
{
|
|
_sources.Clear();
|
|
_sources.AddRange( source );
|
|
|
|
var changed = false;
|
|
|
|
// Remove items
|
|
|
|
while ( _items.Count > _sources.Count )
|
|
{
|
|
var item = _items.Pop();
|
|
_removeAction?.Invoke( item );
|
|
|
|
changed = true;
|
|
}
|
|
|
|
// Add items
|
|
|
|
while ( _items.Count < _sources.Count )
|
|
{
|
|
_items.Add( _addFunc( _sources[_items.Count] ) );
|
|
|
|
changed = true;
|
|
}
|
|
|
|
if ( _updateAction is { } updateAction )
|
|
{
|
|
for ( var i = 0; i < _sources.Count; ++i )
|
|
{
|
|
var item = _items[i];
|
|
|
|
if ( updateAction( _sources[i], ref item ) )
|
|
{
|
|
_items[i] = item;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
public int IndexOf( TSrc src ) => _sources.IndexOf( src );
|
|
|
|
public IEnumerator<TItem> GetEnumerator() => _items.GetEnumerator();
|
|
IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator();
|
|
|
|
public int Count => _items.Count;
|
|
|
|
public TItem this[int index] => _items[index];
|
|
}
|