Editor/AudioToolHandlers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Editor;
using Sandbox;
using Sandbox.Audio;
namespace SboxMcpServer;
/// <summary>
/// Audio MCP tools: create_sound_point, configure_sound.
/// </summary>
internal static class AudioToolHandlers
{
private static readonly JsonSerializerOptions _json = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
// ── create_sound_point ───────────────────────────────────────────────────
internal static object CreateSoundPoint( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
float x = OzmiumSceneHelpers.Get( args, "x", 0f );
float y = OzmiumSceneHelpers.Get( args, "y", 0f );
float z = OzmiumSceneHelpers.Get( args, "z", 0f );
string name = OzmiumSceneHelpers.Get( args, "name", "Sound Point" );
string sound = OzmiumSceneHelpers.Get( args, "soundEvent", (string)null );
float vol = OzmiumSceneHelpers.Get( args, "volume", 1f );
float pitch = OzmiumSceneHelpers.Get( args, "pitch", 1f );
bool play = OzmiumSceneHelpers.Get( args, "playOnStart", true );
bool repeat = OzmiumSceneHelpers.Get( args, "repeat", false );
try
{
var go = scene.CreateObject();
go.Name = name;
go.WorldPosition = new Vector3( x, y, z );
var snd = go.Components.Create<SoundPointComponent>();
if ( !string.IsNullOrEmpty( sound ) )
{
var asset = AssetSystem.FindByPath( sound );
if ( asset != null )
{
var ev = asset.LoadResource<SoundEvent>();
if ( ev != null ) snd.SoundEvent = ev;
}
}
snd.Volume = vol;
snd.Pitch = pitch;
snd.PlayOnStart = play;
snd.Repeat = repeat;
return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
{
message = $"Created SoundPoint '{go.Name}'.",
id = go.Id.ToString(),
position = OzmiumSceneHelpers.V3( go.WorldPosition )
}, _json ) );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── configure_sound ─────────────────────────────────────────────────────
internal static object ConfigureSound( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
string id = OzmiumSceneHelpers.Get( args, "id", (string)null );
string name = OzmiumSceneHelpers.Get( args, "name", (string)null );
var go = OzmiumSceneHelpers.FindGoWithComponent<BaseSoundComponent>( scene, id, name );
if ( go == null ) return OzmiumSceneHelpers.Txt( $"No object with BaseSoundComponent found (id={id ?? "null"}, name={name ?? "null"})." );
var snd = go.Components.Get<BaseSoundComponent>();
try
{
if ( args.TryGetProperty( "soundEvent", out var seEl ) && seEl.ValueKind == JsonValueKind.String )
{
var asset = AssetSystem.FindByPath( seEl.GetString() );
if ( asset != null )
{
var ev = asset.LoadResource<SoundEvent>();
if ( ev != null ) snd.SoundEvent = ev;
}
}
if ( args.TryGetProperty( "volume", out var vEl ) )
snd.Volume = vEl.GetSingle();
if ( args.TryGetProperty( "pitch", out var pEl ) )
snd.Pitch = pEl.GetSingle();
if ( args.TryGetProperty( "playOnStart", out var posEl ) )
snd.PlayOnStart = posEl.GetBoolean();
if ( args.TryGetProperty( "repeat", out var repEl ) )
snd.Repeat = repEl.GetBoolean();
if ( args.TryGetProperty( "distanceAttenuation", out var daEl ) )
snd.DistanceAttenuation = daEl.GetBoolean();
if ( args.TryGetProperty( "distance", out var dEl ) )
snd.Distance = dEl.GetSingle();
return OzmiumSceneHelpers.Txt( $"Configured sound on '{go.Name}' ({snd.GetType().Name})." );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── Schemas ─────────────────────────────────────────────────────────────
private static Dictionary<string, object> S( string name, string desc, Dictionary<string, object> props, string[] req = null )
{
var schema = new Dictionary<string, object> { ["type"] = "object", ["properties"] = props };
if ( req != null ) schema["required"] = req;
return new Dictionary<string, object> { ["name"] = name, ["description"] = desc, ["inputSchema"] = schema };
}
internal static Dictionary<string, object> SchemaCreateSoundPoint => S( "create_sound_point",
"Creates a GO with a SoundPointComponent for spatial audio.",
new Dictionary<string, object>
{
["x"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
["y"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
["z"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
["soundEvent"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Sound event path." },
["volume"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Volume (0-1)." },
["pitch"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Pitch (0-2)." },
["playOnStart"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Play on start (default true)." },
["repeat"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Repeat sound." }
} );
internal static Dictionary<string, object> SchemaConfigureSound => S( "configure_sound",
"Configures an existing BaseSoundComponent on a GameObject.",
new Dictionary<string, object>
{
["id"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
["soundEvent"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Sound event path." },
["volume"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Volume (0-1)." },
["pitch"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Pitch (0-2)." },
["playOnStart"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Play on start." },
["repeat"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Repeat sound." },
["distanceAttenuation"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Enable distance attenuation." },
["distance"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Distance attenuation distance." }
} );
// ── create_soundscape_trigger ────────────────────────────────────────────
internal static object CreateSoundscapeTrigger( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
float x = OzmiumSceneHelpers.Get( args, "x", 0f );
float y = OzmiumSceneHelpers.Get( args, "y", 0f );
float z = OzmiumSceneHelpers.Get( args, "z", 0f );
string name = OzmiumSceneHelpers.Get( args, "name", "Soundscape Trigger" );
string triggerType = OzmiumSceneHelpers.Get( args, "type", "Sphere" );
try
{
var go = scene.CreateObject();
go.Name = name;
go.WorldPosition = new Vector3( x, y, z );
var st = go.Components.Create<SoundscapeTrigger>();
if ( Enum.TryParse<SoundscapeTrigger.TriggerType>( triggerType, true, out var tt ) )
st.Type = tt;
st.Volume = OzmiumSceneHelpers.Get( args, "volume", 1.0f );
st.Radius = OzmiumSceneHelpers.Get( args, "radius", 500f );
st.StayActiveOnExit = OzmiumSceneHelpers.Get( args, "stayActiveOnExit", true );
if ( args.TryGetProperty( "boxSize", out var bsEl ) && bsEl.ValueKind == JsonValueKind.Object )
{
st.BoxSize = new Vector3(
OzmiumSceneHelpers.Get( bsEl, "x", 50f ),
OzmiumSceneHelpers.Get( bsEl, "y", 50f ),
OzmiumSceneHelpers.Get( bsEl, "z", 50f ) );
}
if ( args.TryGetProperty( "soundscapePath", out var spEl ) && spEl.ValueKind == JsonValueKind.String )
{
var asset = AssetSystem.FindByPath( spEl.GetString() );
if ( asset != null )
{
var scape = asset.LoadResource<Soundscape>();
if ( scape != null ) st.Soundscape = scape;
}
}
return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
{
message = $"Created SoundscapeTrigger '{go.Name}'.",
id = go.Id.ToString(),
position = OzmiumSceneHelpers.V3( go.WorldPosition ),
type = st.Type.ToString(),
volume = st.Volume
}, _json ) );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── create_sound_box ─────────────────────────────────────────────────────
internal static object CreateSoundBox( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
float x = OzmiumSceneHelpers.Get( args, "x", 0f );
float y = OzmiumSceneHelpers.Get( args, "y", 0f );
float z = OzmiumSceneHelpers.Get( args, "z", 0f );
string name = OzmiumSceneHelpers.Get( args, "name", "Sound Box" );
try
{
var go = scene.CreateObject();
go.Name = name;
go.WorldPosition = new Vector3( x, y, z );
var sb = go.Components.Create<SoundBoxComponent>();
sb.Volume = OzmiumSceneHelpers.Get( args, "volume", 1f );
sb.PlayOnStart = OzmiumSceneHelpers.Get( args, "playOnStart", true );
sb.Repeat = OzmiumSceneHelpers.Get( args, "repeat", false );
if ( args.TryGetProperty( "boxSize", out var bsEl ) && bsEl.ValueKind == JsonValueKind.Object )
{
sb.Scale = new Vector3(
OzmiumSceneHelpers.Get( bsEl, "x", 50f ),
OzmiumSceneHelpers.Get( bsEl, "y", 50f ),
OzmiumSceneHelpers.Get( bsEl, "z", 50f ) );
}
if ( args.TryGetProperty( "soundEvent", out var seEl ) && seEl.ValueKind == JsonValueKind.String )
{
var asset = AssetSystem.FindByPath( seEl.GetString() );
if ( asset != null )
{
var ev = asset.LoadResource<SoundEvent>();
if ( ev != null ) sb.SoundEvent = ev;
}
}
return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
{
message = $"Created SoundBox '{go.Name}'.",
id = go.Id.ToString(),
position = OzmiumSceneHelpers.V3( go.WorldPosition ),
boxSize = new { sb.Scale.x, sb.Scale.y, sb.Scale.z }
}, _json ) );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── create_dsp_volume ────────────────────────────────────────────────────
internal static object CreateDspVolume( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
float x = OzmiumSceneHelpers.Get( args, "x", 0f );
float y = OzmiumSceneHelpers.Get( args, "y", 0f );
float z = OzmiumSceneHelpers.Get( args, "z", 0f );
string name = OzmiumSceneHelpers.Get( args, "name", "DSP Volume" );
string volumeType = OzmiumSceneHelpers.Get( args, "volumeType", "Box" );
try
{
var go = scene.CreateObject();
go.Name = name;
go.WorldPosition = new Vector3( x, y, z );
var dsp = go.Components.Create<DspVolume>();
dsp.Priority = OzmiumSceneHelpers.Get( args, "priority", 0 );
string targetMixer = OzmiumSceneHelpers.Get( args, "targetMixer", "Game" );
if ( !string.IsNullOrEmpty( targetMixer ) )
dsp.TargetMixer = new MixerHandle { Name = targetMixer };
// Configure volume shape
var sv = dsp.SceneVolume;
if ( Enum.TryParse<Sandbox.Volumes.SceneVolume.VolumeTypes>( volumeType, true, out var vt ) )
sv.Type = vt;
if ( sv.Type == Sandbox.Volumes.SceneVolume.VolumeTypes.Box )
{
if ( args.TryGetProperty( "boxSize", out var bsEl ) && bsEl.ValueKind == JsonValueKind.Object )
{
sv.Box = BBox.FromPositionAndSize( 0,
new Vector3(
OzmiumSceneHelpers.Get( bsEl, "x", 200f ),
OzmiumSceneHelpers.Get( bsEl, "y", 200f ),
OzmiumSceneHelpers.Get( bsEl, "z", 200f ) ) );
}
}
else if ( sv.Type == Sandbox.Volumes.SceneVolume.VolumeTypes.Sphere )
{
sv.Sphere = new Sphere( 0, OzmiumSceneHelpers.Get( args, "radius", 200f ) );
}
dsp.SceneVolume = sv;
if ( args.TryGetProperty( "dspPreset", out var dpEl ) && dpEl.ValueKind == JsonValueKind.String )
{
dsp.Dsp = new DspPresetHandle { Name = dpEl.GetString() };
}
return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
{
message = $"Created DspVolume '{go.Name}'.",
id = go.Id.ToString(),
position = OzmiumSceneHelpers.V3( go.WorldPosition ),
volumeType = sv.Type.ToString()
}, _json ) );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
// ── Audio extension schemas ─────────────────────────────────────────────
internal static Dictionary<string, object> SchemaCreateSoundscapeTrigger => S( "create_soundscape_trigger",
"Create a GO with a SoundscapeTrigger for ambient audio zones (outdoor birds, indoor machinery, cave reverb).",
new Dictionary<string, object>
{
["x"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
["y"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
["z"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
["type"] = new Dictionary<string, object>
{
["type"] = "string", ["description"] = "Trigger type.",
["enum"] = new[] { "Point", "Sphere", "Box" }
},
["soundscapePath"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Soundscape asset path." },
["volume"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Volume (default 1.0)." },
["radius"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Radius for Sphere type (default 500)." },
["boxSize"] = new Dictionary<string, object> { ["type"] = "object", ["description"] = "Box half-extents {x,y,z} for Box type." },
["stayActiveOnExit"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Keep playing after exiting (default true)." }
} );
internal static Dictionary<string, object> SchemaCreateSoundBox => S( "create_sound_box",
"Create a GO with a SoundBoxComponent for area ambient sounds (machinery hum, wind in corridor).",
new Dictionary<string, object>
{
["x"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
["y"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
["z"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
["soundEvent"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Sound event path." },
["volume"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Volume (0-1)." },
["playOnStart"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Play on start (default true)." },
["repeat"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Repeat sound." },
["boxSize"] = new Dictionary<string, object> { ["type"] = "object", ["description"] = "Box half-extents {x,y,z} (default 50,50,50)." }
} );
internal static Dictionary<string, object> SchemaCreateDspVolume => S( "create_dsp_volume",
"Create a GO with a DspVolume for audio effect zones (reverb in halls, lowpass underwater).",
new Dictionary<string, object>
{
["x"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
["y"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
["z"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
["dspPreset"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "DSP preset asset path." },
["targetMixer"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Target mixer name (default 'Game')." },
["priority"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Priority (default 0)." },
["volumeType"] = new Dictionary<string, object>
{
["type"] = "string", ["description"] = "Volume shape type.",
["enum"] = new[] { "Box", "Sphere", "Infinite" }
},
["boxSize"] = new Dictionary<string, object> { ["type"] = "object", ["description"] = "Box size {x,y,z} (default 200,200,200)." },
["radius"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "Sphere radius (default 200)." }
} );
// ── create_audio_listener ─────────────────────────────────────────────
internal static object CreateAudioListener( JsonElement args )
{
var scene = OzmiumSceneHelpers.ResolveScene();
if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );
float x = OzmiumSceneHelpers.Get( args, "x", 0f );
float y = OzmiumSceneHelpers.Get( args, "y", 0f );
float z = OzmiumSceneHelpers.Get( args, "z", 0f );
string name = OzmiumSceneHelpers.Get( args, "name", "Audio Listener" );
try
{
var go = scene.CreateObject();
go.Name = name;
go.WorldPosition = new Vector3( x, y, z );
var listener = go.Components.Create<AudioListener>();
listener.UseCameraDirection = OzmiumSceneHelpers.Get( args, "useCameraDirection", true );
return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
{
message = $"Created AudioListener '{go.Name}'.",
id = go.Id.ToString(),
position = OzmiumSceneHelpers.V3( go.WorldPosition )
}, _json ) );
}
catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
}
internal static Dictionary<string, object> SchemaCreateAudioListener => S( "create_audio_listener",
"Create a GO with an AudioListener for custom audio origin points. Defines where the player 'hears' from — useful for security cameras, cutscenes.",
new Dictionary<string, object>
{
["x"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World X position." },
["y"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Y position." },
["z"] = new Dictionary<string, object> { ["type"] = "number", ["description"] = "World Z position." },
["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Name for the GO." },
["useCameraDirection"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Use camera direction for audio (default true)." }
} );
}