GolfCupShape.cs

Component for a golf cup piece that selects between three cup models (Straight, Curved, Trench), keeps the ModelRenderer and ModelCollider in sync with that choice, and manages a hidden BoxCollider used as the scoring trigger by setting its center, size and trigger/flag state each frame. It detects an existing model on enable so manual model choices are preserved.

File Access
using Sandbox;
using System;

/// <summary>
/// The shape of a cup piece: Straight, Curved or Trench. Changing it in the Inspector sets both the Model
/// Renderer's and the Model Collider's model to the matching kit model, so one dropdown swaps the
/// look and the collision, and the hole's merged collider picks the new opening up. On enable it
/// follows whatever model the piece already has, so a model set by hand is left alone.
///
/// It also owns the cup's scoring trigger (the Box Collider on the same object): the trigger's
/// centre and size for the chosen shape are re-applied every frame, in the editor and in play, so
/// a change by hand (the Inspector's Set To GameObject Bounds, say) snaps straight back, and the
/// collider is hidden from the Inspector.
/// </summary>
[Title( "Golf Cup Shape" ), Icon( "sports_golf" )]
public sealed class GolfCupShape : Component, Component.ExecuteInEditor
{
	public enum CupShapeKind
	{
		Straight,
		Curved,
		Trench,
	}

	public const string StraightModel = "models/kit/cup_straight.vmdl";
	public const string CurvedModel = "models/kit/cup_curved.vmdl";
	public const string TrenchModel = "models/kit/trench/trench_hole.vmdl";

	/// <summary>Straight drops the ball straight into the cup; Curved has a bowl that funnels it in; Trench is the clay trench tile with a funnel.</summary>
	[Property, Title( "Cup Shape" )]
	public CupShapeKind Shape { get; set; } = CupShapeKind.Straight;

	CupShapeKind? _applied;

	protected override void OnEnabled()
	{
		// Start from the model the piece carries, so a model chosen by hand isn't overwritten.
		var current = Detect();
		if ( current.HasValue ) Shape = current.Value;
		_applied = Shape;
	}

	protected override void OnUpdate()
	{
		if ( _applied != Shape ) Apply();
		OwnTrigger();
	}

	/// <summary>The scoring trigger's centre and size for a shape: a small box at the bottom of the cup.</summary>
	public static (Vector3 Center, Vector3 Size) TriggerFor( CupShapeKind shape ) => shape switch
	{
		CupShapeKind.Straight => (new Vector3( 0f, 0f, -40f ), new Vector3( 8f, 8f, 14f )),
		_ => (new Vector3( 0f, 0f, -52f ), new Vector3( 8f, 8f, 14f )),   // the curved bowl and the trench funnel are 60 deep
	};

	/// <summary>Puts the trigger box where this shape wants it, keeps it a trigger, and keeps it out of the Inspector.</summary>
	void OwnTrigger()
	{
		var box = GetComponent<BoxCollider>( true );
		if ( box is null ) return;

		var (center, size) = TriggerFor( Shape );
		if ( box.Center != center ) box.Center = center;
		if ( box.Scale != size ) box.Scale = size;
		if ( !box.IsTrigger ) box.IsTrigger = true;

		const ComponentFlags owned = ComponentFlags.Hidden | ComponentFlags.NotEditable;
		if ( (box.Flags & owned) != owned ) box.Flags |= owned;
	}

	CupShapeKind? Detect()
	{
		var name = GetComponent<ModelRenderer>( true )?.Model?.Name;
		if ( string.IsNullOrEmpty( name ) ) return null;
		if ( name.Contains( "trench", StringComparison.OrdinalIgnoreCase ) ) return CupShapeKind.Trench;
		return name.Contains( "curved", StringComparison.OrdinalIgnoreCase ) ? CupShapeKind.Curved : CupShapeKind.Straight;
	}

	void Apply()
	{
		var model = Model.Load( Shape switch { CupShapeKind.Curved => CurvedModel, CupShapeKind.Trench => TrenchModel, _ => StraightModel } );
		if ( model is null ) return;

		var renderer = GetComponent<ModelRenderer>( true );
		if ( renderer is not null )
		{
			renderer.Model = model;
			// The trench tile wears its own clay; the kit cups' grass override would hide it.
			if ( Shape == CupShapeKind.Trench ) renderer.MaterialOverride = null;
		}
		var collider = GetComponent<ModelCollider>( true );
		if ( collider is not null ) collider.Model = model;

		_applied = Shape;
		GameObject.GetComponentInParent<GolfHoleCollision>( true )?.MarkDirty();
	}
}