Files
sbox-public/game/editor/MovieMaker/Code/Session/Mapping.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

333 lines
9.2 KiB
C#

using System.Linq;
using System.Reflection;
using Sandbox.MovieMaker;
using Sandbox.MovieMaker.Properties;
namespace Editor.MovieMaker;
#nullable enable
partial class Session
{
public IProjectTrack? GetTrack( GameObject go )
{
return Project.Tracks
.OfType<ProjectReferenceTrack<GameObject>>()
.FirstOrDefault( x => Binder.Get( x ) is { IsBound: true } binder && binder.Value == go );
}
public IProjectTrack? GetTrack( Component cmp )
{
return Project.Tracks
.OfType<IProjectReferenceTrack>()
.FirstOrDefault( x => Binder.Get( x ) is { IsBound: true } binder && binder.Value == cmp );
}
public IProjectTrack? GetTrack( GameObject go, string propertyPath )
{
return GetTrack( GetTrack( go ), propertyPath );
}
public IProjectTrack? GetTrack( Component cmp, string propertyPath )
{
return GetTrack( GetTrack( cmp ), propertyPath );
}
private IProjectTrack? GetTrack( IProjectTrack? parentTrack, string propertyPath )
{
while ( parentTrack is not null && propertyPath.Length > 0 )
{
var propertyName = propertyPath;
// TODO: Hack for anim graph parameters including periods
if ( parentTrack.TargetType != typeof( SkinnedModelRenderer.ParameterAccessor ) && propertyPath.IndexOf( '.' ) is var index and > -1 )
{
propertyName = propertyPath[..index];
propertyPath = propertyPath[(index + 1)..];
}
else
{
propertyPath = string.Empty;
}
parentTrack = parentTrack.Children.FirstOrDefault( x => x.Name == propertyName );
}
return parentTrack;
}
public ProjectSequenceTrack? GetTrack( MovieResource resource )
{
return Project.Tracks
.OfType<ProjectSequenceTrack>()
.FirstOrDefault( x => x.Blocks.Any( y => y.Resource == resource ) );
}
public IProjectTrack GetOrCreateTrack( GameObject go )
{
if ( GetTrack( go ) is { } existing ) return existing;
IProjectTrack? parentTrack = null;
if ( go.Parent is { } parentGo and not Scene )
{
// Procedural bone objects need a parent track
if ( (go.Flags & GameObjectFlags.Bone) != 0 )
{
parentTrack = GetOrCreateTrack( parentGo );
}
// Otherwise, if parent has a track, use it
else
{
parentTrack = GetTrack( parentGo );
}
}
var track = Project.AddReferenceTrack( go.Name, typeof(GameObject), parentTrack );
track.ReferenceId = go.Id;
Binder.Get( track ).Bind( go );
// If we have root tracks for child objects, parent them to the new track
foreach ( var child in go.Children )
{
if ( GetTrack( child ) is IProjectTrackInternal { Parent: null } childTrack )
{
((IProjectTrackInternal)track).AddChild( childTrack );
}
}
return track;
}
public IProjectTrack GetOrCreateTrack( Component cmp )
{
if ( GetTrack( cmp ) is { } existing ) return existing;
// Nest component tracks inside the containing game object's track
var goTrack = GetOrCreateTrack( cmp.GameObject );
var track = Project.AddReferenceTrack( cmp.GetType().Name, cmp.GetType(), goTrack );
track.ReferenceId = cmp.Id;
Binder.Get( track ).Bind( cmp );
return track;
}
public IProjectTrack GetOrCreateTrack( GameObject go, string propertyPath )
{
if ( GetTrack( go, propertyPath ) is { } existing ) return existing;
// Nest property tracks inside the containing GameObject's track
return GetOrCreateTrack( GetOrCreateTrack( go ), propertyPath );
}
public IProjectTrack GetOrCreateTrack( Component cmp, string propertyPath )
{
if ( GetTrack( cmp, propertyPath ) is { } existing ) return existing;
// Nest property tracks inside the containing Component's track
return GetOrCreateTrack( GetOrCreateTrack( cmp ), propertyPath );
}
public ProjectSequenceTrack GetOrCreateTrack( MovieResource resource )
{
if ( GetTrack( resource ) is { } existing ) return existing;
return Project.AddSequenceTrack( $"{resource.ResourceName.ToTitleCase()} Sequence" );
}
public IProjectTrack GetOrCreateTrack( IProjectTrack parentTrack, string propertyPath )
{
while ( propertyPath.Length > 0 )
{
var propertyName = propertyPath;
// TODO: Hack for anim graph parameters including periods
if ( parentTrack.TargetType != typeof( SkinnedModelRenderer.ParameterAccessor ) && propertyPath.IndexOf( '.' ) is var index and > -1 )
{
propertyName = propertyPath[..index];
propertyPath = propertyPath[(index + 1)..];
}
else
{
propertyPath = string.Empty;
}
parentTrack = GetOrCreateTrackCore( parentTrack, propertyName );
}
return parentTrack;
}
/// <summary>
/// Create a track hierarchy matching the given <paramref name="preset"/>, rooted on <paramref name="rootTrack"/>.
/// </summary>
public void LoadPreset( IProjectTrack rootTrack, TrackPresetNode preset )
{
var rootGo = (Binder.Get( rootTrack ) as ITrackReference<GameObject>)?.Value;
foreach ( var childPreset in preset.Children )
{
if ( GetOrCreatePresetTrackCore( rootTrack, childPreset, rootGo ) is { } childTrack )
{
LoadPreset( childTrack, childPreset );
}
}
}
public void RemovePreset( IProjectTrack rootTrack, TrackPresetNode preset )
{
foreach ( var childPreset in preset.Children )
{
if ( rootTrack.Children.FirstOrDefault( x => x.Name == childPreset.PropertyName ) is not { } childTrack ) continue;
if ( !childTrack.TargetType.IsAssignableTo( childPreset.PropertyType ) ) continue;
RemovePreset( childTrack, childPreset );
if ( childTrack.IsEmpty )
{
childTrack.Remove();
}
}
}
private IProjectTrack? GetOrCreatePresetTrackCore( IProjectTrack rootTrack, TrackPresetNode childPreset, GameObject? rootGameObject )
{
if ( rootGameObject is null )
{
return GetOrCreateTrack( rootTrack, childPreset.PropertyName );
}
if ( childPreset.PropertyType == typeof( GameObject ) )
{
var child = rootGameObject.Children.FirstOrDefault( x => x.Name == childPreset.PropertyName );
return child is null ? null : GetOrCreateTrack( child );
}
if ( childPreset.PropertyType.IsAssignableTo( typeof( Component ) ) )
{
var component = rootGameObject.Components.FirstOrDefault( childPreset.PropertyType.IsInstanceOfType );
return component is null ? null : GetOrCreateTrack( component );
}
return GetOrCreateTrack( rootTrack, childPreset.PropertyName );
}
private IProjectTrack GetOrCreateTrackCore( IProjectTrack parentTrack, string propertyName )
{
if ( parentTrack.Children.FirstOrDefault( x => x.Name == propertyName ) is { } existingTrack )
{
return existingTrack;
}
if ( Binder.Get( parentTrack ) is not { } parentProperty )
{
throw new Exception( "Parent track not registered." );
}
var property = TrackProperty.Create( parentProperty, propertyName )
?? throw new Exception( $"Unknown property \"{propertyName}\" in type \"{parentProperty.TargetType}\"." );
return Project.AddPropertyTrack( property.Name, property.TargetType, parentTrack );
}
private readonly HashSet<SkinnedModelRenderer> _controlledSkinnedModelRenderers = new();
private IEnumerable<T> GetControlled<T>()
where T : Component
{
// When rendering, the whole scene is considered part of the movie.
// Otherwise, we're just previewing playback in the editor, so only consider
// stuff we've explicitly bound to this movie.
return Renderer.IsRendering
? Player.Scene.GetAll<T>()
: Binder.GetComponents<T>( Project );
}
/// <summary>
/// Advance all bound <see cref="SkinnedModelRenderer"/>s by the given <paramref name="deltaTime"/>.
/// </summary>
public void AdvanceAnimations( MovieTime deltaTime )
{
// Negative deltas aren't supported :(
var dt = Math.Min( (float)deltaTime.Absolute.TotalSeconds, 1f );
Time.Delta = dt;
using var sceneScope = Player.Scene.Push();
_controlledSkinnedModelRenderers.Clear();
foreach ( var controller in GetControlled<PlayerController>() )
{
controller.MovieEditorFixedUpdate();
if ( controller.Renderer is not { } renderer ) continue;
_controlledSkinnedModelRenderers.Add( renderer );
controller.UpdateAnimation( renderer );
}
foreach ( var renderer in GetControlled<SkinnedModelRenderer>() )
{
_controlledSkinnedModelRenderers.Add( renderer );
}
foreach ( var renderer in _controlledSkinnedModelRenderers )
{
UpdateAnimationPlaybackRate( renderer, dt );
}
}
private void UpdateAnimationPlaybackRate( SkinnedModelRenderer renderer, float dt )
{
if ( renderer.SceneModel is not { } model ) return;
if ( dt > 0f && IsEditorScene )
{
model.PlaybackRate = renderer.PlaybackRate;
model.Update( dt );
}
model.PlaybackRate = IsEditorScene ? 0f : 1f;
}
}
file static class PlayerControllerExtensions
{
private static Action<PlayerController> UpdateHeadroom { get; } = typeof( PlayerController )
.GetMethod( nameof( UpdateHeadroom ), BindingFlags.Instance | BindingFlags.NonPublic )!
.CreateDelegate<Action<PlayerController>>();
private static Action<PlayerController> UpdateFalling { get; } = typeof( PlayerController )
.GetMethod( nameof( UpdateFalling ), BindingFlags.Instance | BindingFlags.NonPublic )!
.CreateDelegate<Action<PlayerController>>();
public static void MovieEditorFixedUpdate( this PlayerController controller )
{
IScenePhysicsEvents physicsEvents = controller;
physicsEvents.PrePhysicsStep();
physicsEvents.PostPhysicsStep();
UpdateHeadroom( controller );
UpdateFalling( controller );
}
}