GameLoop/GameManager.ContextMenu.cs
public sealed partial class GameManager : IContextMenuEvent
{
	/// <summary>
	/// The built-in Inspector right-click options: Ignite/Extinguish, Delete and Break.
	/// Addons add their own by implementing <see cref="IContextMenuEvent"/> the same way.
	/// </summary>
	void IContextMenuEvent.PopulateContextMenu( IContextMenuEvent.Event e )
	{
		var target = e.Target;
		if ( !target.IsValid() ) return;

		var isPlayer = target.Tags.Has( "player" );
		var prop = target.GetComponent<Prop>();

		if ( IsOnFire( target ) )
		{
			e.AddOption( "🧯", Game.Language.GetPhrase( "spawnmenu.inspect.extinguish" ), () => ExtinguishInspectedObject( target ), 100 );
		}
		else if ( !isPlayer )
		{
			e.AddOption( "🔥", Game.Language.GetPhrase( "spawnmenu.inspect.ignite" ), () => IgniteInspectedObject( target ), 100 );
		}

		if ( !isPlayer )
		{
			e.AddOption( "🗑️", Game.Language.GetPhrase( "spawnmenu.inspect.delete" ), () => DeleteInspectedObject( target ), 200 );
		}

		if ( prop.IsValid() && prop.Health > 0 )
		{
			e.AddOption( "💥", Game.Language.GetPhrase( "spawnmenu.inspect.break" ), () => BreakInspectedProp( prop ), 300 );
		}
	}

	/// <summary>
	/// Delete an object from the Inspector context menu.
	/// </summary>
	[Rpc.Host]
	internal static void DeleteInspectedObject( GameObject go )
	{
		if ( !go.IsValid() || go.IsProxy ) return;
		if ( go.Tags.Has( "player" ) ) return;

		// Check ownership if the object has an Ownable component
		if ( !go.HasAccess( Rpc.Caller ) ) return;

		go.Destroy();
	}

	/// <summary>
	/// Break (gib) a prop from the Inspector context menu.
	/// </summary>
	[Rpc.Host]
	internal static void BreakInspectedProp( Prop prop )
	{
		if ( !prop.IsValid() || prop.IsProxy ) return;
		// Check ownership if the object has an Ownable component
		if ( !prop.GameObject.HasAccess( Rpc.Caller ) ) return;

		var damageable = prop.GetComponent<Component.IDamageable>();
		if ( damageable is null ) return;

		var dmg = new DamageInfo( 999999, null, null );
		dmg.Tags.Add( DamageTags.GibAlways );
		damageable.OnDamage( in dmg );
	}

	/// <summary>
	/// Set an object on fire from the Inspector context menu. The engine's FireDamage component
	/// damages every IDamageable under the root until it breaks. Objects that can't break (unbreakable
	/// props, non-damageable entities) just keep burning.
	/// </summary>
	[Rpc.Host]
	internal static void IgniteInspectedObject( GameObject go )
	{
		if ( !go.IsValid() || go.IsProxy ) return;
		if ( go.Tags.Has( "player" ) ) return;
		if ( !go.HasAccess( Rpc.Caller ) ) return;
		if ( IsOnFire( go ) ) return;

		// Props already know how to burn. Prop.IsOnFire is never reset (protected setter), so a prop
		// that was extinguished and re-ignited falls through to the generic path below.
		if ( go.GetComponent<Prop>() is { IsOnFire: false } prop )
		{
			prop.Ignite();
			return;
		}

		// Everything else gets the same fire prefab Prop.Ignite uses, parented to the root
		var firePrefab = ResourceLibrary.Get<PrefabFile>( "/prefabs/engine/ignite.prefab" );
		if ( firePrefab is null )
		{
			Log.Warning( "Can't find /prefabs/engine/ignite.prefab" );
			return;
		}

		var fire = GameObject.Clone( firePrefab, new CloneConfig { Parent = go, Transform = global::Transform.Zero, StartEnabled = true } );
		if ( !fire.IsValid() ) return;

		fire.RunEvent<ParticleModelEmitter>( x => x.Target = go );

		if ( fire.Network.Active )
		{
			fire.Network.Refresh( fire );
		}
	}

	/// <summary>
	/// Put out a burning object from the Inspector context menu. Removes every fire effect
	/// (anything carrying a FireDamage) under the root; descendant destruction is networked.
	/// </summary>
	[Rpc.Host]
	internal static void ExtinguishInspectedObject( GameObject go )
	{
		if ( !go.IsValid() || go.IsProxy ) return;
		if ( !go.HasAccess( Rpc.Caller ) ) return;

		foreach ( var fire in go.GetComponentsInChildren<FireDamage>( true ).ToArray() )
		{
			if ( !fire.IsValid() ) continue;

			var fireObject = fire.GameObject;
			fireObject.Destroy();

			// Destroying a descendant is only broadcast when we refresh it
			if ( go.Network.Active )
			{
				go.Network.Refresh( fireObject );
			}
		}
	}

	/// <summary>
	/// Is this object currently burning? Works on clients too, since the fire prefab is replicated
	/// as a child of the root. Deliberately not Prop.IsOnFire: that flag is never cleared.
	/// </summary>
	internal static bool IsOnFire( GameObject go )
	{
		if ( !go.IsValid() ) return false;
		return go.GetComponentInChildren<FireDamage>( true ).IsValid();
	}
}