Editor helper that drives camera and player navigation in the scene editor. It resolves the active Scene, finds camera or player controller components, and implements actions: status, frame, look_at, move/fly/set_transform, and raycast/probe, returning structured ToolOutcome results.
#nullable enable
using System;
using System.Linq;
using Sandbox;
namespace Editor.Mcp
{
internal static class CameraNavigationDriver
{
private static Scene? ResolveScene()
{
if ( Game.ActiveScene != null ) return Game.ActiveScene;
var session = EditorRuntime.GetStaticProperty( "Editor.SceneEditorSession", "Active" );
if ( session != null )
{
return EditorRuntime.GetMember( session, "Scene" ) as Scene;
}
return null;
}
public static ToolOutcome<object> Navigate(
string action = "status",
string? target = null,
float? posX = null,
float? posY = null,
float? posZ = null,
float? pitch = null,
float? yaw = null,
float? roll = null,
float? deltaX = null,
float? deltaY = null,
float? deltaZ = null,
float maxRayDistance = 5000f )
{
var scene = ResolveScene();
if ( scene == null )
{
return ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "nav.no_scene", "No active scene loaded." );
}
var normAction = action.ToLowerInvariant().Trim();
// 1. Frame / Focus Action
if ( normAction == "frame" )
{
if ( string.IsNullOrWhiteSpace( target ) )
{
return ToolOutcome<object>.Failure( null, ToolStatus.Failed, "nav.missing_target", "Target GameObject name is required to frame." );
}
var go = scene.Directory.FindByGuid( Guid.TryParse( target, out var guid ) ? guid : Guid.Empty )
?? scene.GetAllObjects( false ).FirstOrDefault( o => string.Equals( o.Name, target, StringComparison.OrdinalIgnoreCase ) );
if ( go == null )
{
return ToolOutcome<object>.Failure( new { Target = target }, ToolStatus.Degraded, "nav.target_not_found", $"Target '{target}' was not found in scene." );
}
var session = Editor.SceneEditorSession.Active;
if ( session != null )
{
var center = go.WorldPosition;
var modelRenderer = go.GetComponent<ModelRenderer>();
var bounds = new BBox( center - new Vector3( 60f, 60f, 60f ), center + new Vector3( 60f, 60f, 60f ) );
if ( modelRenderer != null && modelRenderer.Model != null )
{
bounds = modelRenderer.Model.Bounds.Translate( center );
}
session.FrameTo( bounds );
}
return ToolOutcome<object>.Ready( new
{
Action = "frame",
Target = go.Name,
TargetPosition = go.WorldPosition,
Framed = true
} );
}
// Find primary camera or player controller
var cameraComponent = scene.GetAllComponents<CameraComponent>().FirstOrDefault();
var playerController = scene.GetAllComponents<PlayerController>().FirstOrDefault();
var currentPos = cameraComponent?.WorldPosition ?? playerController?.WorldPosition ?? Vector3.Zero;
var currentRot = cameraComponent?.WorldRotation ?? playerController?.WorldRotation ?? Rotation.Identity;
var currentAngles = currentRot.Angles();
// 2. Look At Target Action
if ( normAction == "look_at" )
{
Vector3 targetPos = Vector3.Zero;
string resolvedTargetName = "Coordinates";
if ( !string.IsNullOrWhiteSpace( target ) )
{
var go = scene.Directory.FindByGuid( Guid.TryParse( target, out var g ) ? g : Guid.Empty )
?? scene.GetAllObjects( false ).FirstOrDefault( o => string.Equals( o.Name, target, StringComparison.OrdinalIgnoreCase ) );
if ( go == null )
{
return ToolOutcome<object>.Failure( new { Target = target }, ToolStatus.Degraded, "nav.target_not_found", $"Target '{target}' not found." );
}
targetPos = go.WorldPosition;
resolvedTargetName = go.Name;
}
else if ( posX.HasValue && posY.HasValue && posZ.HasValue )
{
targetPos = new Vector3( posX.Value, posY.Value, posZ.Value );
}
else
{
return ToolOutcome<object>.Failure( null, ToolStatus.Failed, "nav.missing_coords", "Either target name or target (posX, posY, posZ) must be provided for look_at." );
}
var eyePos = playerController != null ? playerController.WorldPosition + Vector3.Up * 64f : currentPos;
var lookRot = Rotation.LookAt( targetPos - eyePos );
var lookAngles = lookRot.Angles();
if ( playerController != null )
{
playerController.EyeAngles = lookAngles;
}
if ( cameraComponent != null )
{
cameraComponent.WorldRotation = lookRot;
}
return ToolOutcome<object>.Ready( new
{
Action = "look_at",
Target = resolvedTargetName,
TargetPosition = targetPos,
SourcePosition = eyePos,
LookAngles = new { lookAngles.pitch, lookAngles.yaw, lookAngles.roll },
Forward = lookRot.Forward
} );
}
// 3. Move / Fly / Look Action
if ( normAction == "move" || normAction == "fly" || normAction == "set_transform" )
{
var newPos = currentPos;
if ( posX.HasValue && posY.HasValue && posZ.HasValue )
{
newPos = new Vector3( posX.Value, posY.Value, posZ.Value );
}
else
{
if ( deltaX.HasValue ) newPos.x += deltaX.Value;
if ( deltaY.HasValue ) newPos.y += deltaY.Value;
if ( deltaZ.HasValue ) newPos.z += deltaZ.Value;
}
var newPitch = pitch ?? currentAngles.pitch;
var newYaw = yaw ?? currentAngles.yaw;
var newRoll = roll ?? currentAngles.roll;
var newAngles = new Angles( Math.Clamp( newPitch, -89f, 89f ), newYaw, newRoll );
var newRot = newAngles.ToRotation();
if ( cameraComponent != null )
{
cameraComponent.WorldPosition = newPos;
cameraComponent.WorldRotation = newRot;
}
else if ( playerController != null )
{
playerController.WorldPosition = newPos;
playerController.EyeAngles = newAngles;
}
return ToolOutcome<object>.Ready( new
{
Action = normAction,
Position = newPos,
Angles = new { newAngles.pitch, newAngles.yaw, newAngles.roll },
Forward = newRot.Forward,
TargetGameObject = cameraComponent?.GameObject.Name ?? playerController?.GameObject.Name
} );
}
// 4. Raycast / Probe Forward Action
if ( normAction == "raycast" || normAction == "probe" )
{
var rayOrigin = playerController != null ? playerController.WorldPosition + Vector3.Up * 64f : currentPos;
var rayDir = playerController != null ? playerController.EyeAngles.Forward : currentRot.Forward;
var rayEnd = rayOrigin + (rayDir * Math.Clamp( maxRayDistance, 10f, 50000f ));
var tr = scene.Trace.Ray( rayOrigin, rayEnd )
.UseHitboxes( true )
.Run();
return ToolOutcome<object>.Ready( new
{
Action = "raycast",
Hit = tr.Hit,
HitGameObject = tr.GameObject?.Name,
HitObjectId = tr.GameObject?.Id.ToString(),
HitDistance = tr.Distance,
HitPosition = tr.EndPosition,
HitNormal = tr.Normal,
RayOrigin = rayOrigin,
RayDirection = rayDir,
Tags = tr.GameObject?.Tags?.ToArray()
} );
}
// Default: Return current Camera / Player navigation status
return ToolOutcome<object>.Ready( new
{
Action = "status",
HasActiveCamera = cameraComponent != null,
CameraName = cameraComponent?.GameObject.Name,
Position = currentPos,
Angles = new { currentAngles.pitch, currentAngles.yaw, currentAngles.roll },
Forward = currentRot.Forward,
IsPlaying = Game.IsPlaying
} );
}
}
}