Code/BaseWireOutputComponent.cs
namespace Sandbox
{
	public class WireOutput
	{
		public object value = 0;
		public GameObject entity;
		public string outputName;
		public string type;
		public List<WireInput> connected = new();
		public int executions = 0;
		public int executionsTick = 0;

		public WireOutput( GameObject entity, string outputName, string type )
		{
			this.entity = entity;
			this.outputName = outputName;
			this.type = type;

			value = IWireComponent.GetDefaultValueFromType( type );
		}
	}

	public readonly struct PortType
	{
		public string Name { get; init; }
		public string Type { get; init; }

		public static PortType Any( string name ) =>
			new() { Name = name, Type = "any" };
		public static PortType Bool( string name ) =>
			new() { Name = name, Type = "bool" };
		public static PortType Int( string name ) =>
			new() { Name = name, Type = "int" };
		public static PortType Float( string name ) =>
			new() { Name = name, Type = "float" };
		public static PortType String( string name ) =>
			new() { Name = name, Type = "string" };
		public static PortType Vector3( string name ) =>
			new() { Name = name, Type = "vector3" };
		public static PortType Angle( string name ) =>
			new() { Name = name, Type = "angle" };
		public static PortType Rotation( string name ) =>
			new() { Name = name, Type = "rotation" };
		public static PortType GameObject( string name ) =>
			new() { Name = name, Type = "gameobject" };
	}

	public interface IWireOutputComponent : IWireComponent
	{
		public void WireTriggerOutput<T>( string outputName, T value )
		{
			var output = GetOutput( outputName );

			// return early if new value is the same as current value, so nothing should trigger
			if ( output.value.Equals( value ) )
				return;

			output.value = value;

			var tick = (Time.Now * 50).FloorToInt();
			if ( output.executionsTick != tick )
			{
				output.executionsTick = tick;
				output.executions = 1;
			}
			else if ( output.executions >= 4 )
			{
				// prevent infinite loops
				return; // todo: queue for next tick?
			}
			else
			{
				output.executions++;
			}

			foreach ( var input in output.connected )
			{
				if ( !input.entity.IsValid() ) continue;
				if ( input.entity.GetComponent<IWireInputComponent>() is IWireInputComponent inputEntity )
				{
					inputEntity.WireTriggerInput( input.inputName, value );
				}
			}
		}

		// Rpc.Broadcast version is over in the BaseWireComponent class
		public void WireConnect( GameObject inputEnt, string outputName, string inputName );
		internal void _WireConnect( IWireInputComponent inputEnt, string outputName, string inputName )
		{
			var input = inputEnt.GetInput( inputName );
			var output = GetOutput( outputName );
			var connected = output.connected;
			if ( input.connectedOutput != null )
			{
				inputEnt._DisconnectInput( inputName );
			}
			input.connectedOutput = output;
			connected.Add( input );
			inputEnt.WireTriggerInput( input.inputName, output.value );
		}

		public WireOutput GetOutput( string inputName )
		{
			if ( WirePorts.outputs.Count == 0 )
			{
				InitializeOutputs();
				WireInitializeOutputs();
			}
			return WirePorts.outputs[inputName];
		}
		public string[] GetOutputNames( bool withValues = false )
		{
			if ( WirePorts.outputs.Count == 0 )
			{
				InitializeOutputs();
				WireInitializeOutputs();
			}
			return !withValues
				? WirePorts.outputs.Keys.ToArray()
				: WirePorts.outputs.Keys.Select( ( string key ) =>
				{
					var type = WirePorts.outputs[key].type;
					if ( type == "string" )
						return $"{key} [{type}]: \"{WirePorts.outputs[key].value}\"";

					return $"{key} [{type}]: {WirePorts.outputs[key].value}";
				} ).ToArray();
		}

		// Entities can implement this for custom output initialization
		public virtual void WireInitializeOutputs() { }

		public void InitializeOutputs()
		{
			foreach ( var type in WireGetOutputs() )
			{
				WirePorts.outputs[type.Name] = new WireOutput( ((Component)this).GameObject, type.Name, type.Type );
			}
		}
		abstract public PortType[] WireGetOutputs();
	}
	
	public abstract class BaseWireOutputComponent: BaseWireComponent, IWireOutputComponent
	{
		public abstract PortType[] WireGetOutputs();
		public virtual string GetOverlayText() { return ""; }
		// Entities can implement this for custom output initialization
		public virtual void WireInitializeOutputs() { }
	}

	public abstract class BaseWireInputOutputComponent: BaseWireComponent, IWireInputComponent, IWireOutputComponent
	{
		public abstract void WireInitialize();
		public abstract PortType[] WireGetOutputs();
		public virtual string GetOverlayText() { return ""; }
		// Entities can implement this for custom output initialization
		public virtual void WireInitializeOutputs() { }
	}

	// Extension methods to allow calling the interface methods without explicit casting
	public static class BaseWireOutputComponentUtils
	{
		public static void WireTriggerOutput<T>( this BaseWireOutputComponent instance, string outputName, T value )
		{
			((IWireOutputComponent)instance).WireTriggerOutput( outputName, value );
		}
		public static WireOutput GetOutput( this BaseWireOutputComponent instance, string inputName )
		{
			return ((IWireOutputComponent)instance).GetOutput( inputName );
		}

		public static void WireTriggerOutput<T>( this BaseWireInputOutputComponent instance, string outputName, T value )
		{
			((IWireOutputComponent)instance).WireTriggerOutput( outputName, value );
		}
		public static WireOutput GetOutput( this BaseWireInputOutputComponent instance, string inputName )
		{
			return ((IWireOutputComponent)instance).GetOutput( inputName );
		}
	}
}