mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-02-15 12:44:58 +00:00
feat: update routing methods
Routing methods will now take a source port and destination port. This should solve the issue where a device could have multiple input ports defined in tielines and allow Essentials routing to find a path correctly.
This commit is contained in:
@@ -12,20 +12,32 @@ namespace PepperDash.Essentials.Core
|
|||||||
/// on those destinations.
|
/// on those destinations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, RouteRequest> RouteRequests = new Dictionary<string, RouteRequest>();
|
private static readonly Dictionary<string, RouteRequest> RouteRequests = new Dictionary<string, RouteRequest>();
|
||||||
/// <summary>
|
|
||||||
/// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute
|
/// <summary>
|
||||||
/// and then attempts a new Route and if sucessful, stores that RouteDescriptor
|
/// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute
|
||||||
/// in RouteDescriptorCollection.DefaultCollection
|
/// and then attempts a new Route and if sucessful, stores that RouteDescriptor
|
||||||
/// </summary>
|
/// in RouteDescriptorCollection.DefaultCollection
|
||||||
public static void ReleaseAndMakeRoute(this IRoutingSink destination, IRoutingOutputs source, eRoutingSignalType signalType)
|
/// </summary>
|
||||||
{
|
public static void ReleaseAndMakeRoute(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, string destinationPortKey = "", string sourcePortKey = "")
|
||||||
var routeRequest = new RouteRequest {
|
{
|
||||||
Destination = destination,
|
var inputPort = string.IsNullOrEmpty(destinationPortKey) ? null : destination.InputPorts.FirstOrDefault(p => p.Key == destinationPortKey);
|
||||||
Source = source,
|
var outputPort = string.IsNullOrEmpty(sourcePortKey) ? null : source.OutputPorts.FirstOrDefault(p => p.Key == sourcePortKey);
|
||||||
SignalType = signalType
|
|
||||||
};
|
ReleaseAndMakeRoute(destination, source, signalType, inputPort, outputPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReleaseAndMakeRoute(IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort = null, RoutingOutputPort sourcePort = null)
|
||||||
|
{
|
||||||
|
var routeRequest = new RouteRequest
|
||||||
|
{
|
||||||
|
Destination = destination,
|
||||||
|
DestinationPort = destinationPort,
|
||||||
|
Source = source,
|
||||||
|
SourcePort = sourcePort,
|
||||||
|
SignalType = signalType
|
||||||
|
};
|
||||||
|
|
||||||
var coolingDevice = destination as IWarmingCooling;
|
var coolingDevice = destination as IWarmingCooling;
|
||||||
|
|
||||||
@@ -65,15 +77,15 @@ namespace PepperDash.Essentials.Core
|
|||||||
|
|
||||||
destination.ReleaseRoute();
|
destination.ReleaseRoute();
|
||||||
|
|
||||||
RunRouteRequest(routeRequest);
|
RunRouteRequest(routeRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunRouteRequest(RouteRequest request)
|
private static void RunRouteRequest(RouteRequest request)
|
||||||
{
|
{
|
||||||
if (request.Source == null)
|
if (request.Source == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var newRoute = request.Destination.GetRouteToSource(request.Source, request.SignalType);
|
var newRoute = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort);
|
||||||
|
|
||||||
if (newRoute == null)
|
if (newRoute == null)
|
||||||
return;
|
return;
|
||||||
@@ -85,13 +97,13 @@ namespace PepperDash.Essentials.Core
|
|||||||
newRoute.ExecuteRoutes();
|
newRoute.ExecuteRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Will release the existing route on the destination, if it is found in
|
/// Will release the existing route on the destination, if it is found in
|
||||||
/// RouteDescriptorCollection.DefaultCollection
|
/// RouteDescriptorCollection.DefaultCollection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="destination"></param>
|
/// <param name="destination"></param>
|
||||||
public static void ReleaseRoute(this IRoutingSink destination)
|
public static void ReleaseRoute(this IRoutingInputs destination)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRequest) && destination is IWarmingCooling)
|
if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRequest) && destination is IWarmingCooling)
|
||||||
{
|
{
|
||||||
@@ -102,150 +114,182 @@ namespace PepperDash.Essentials.Core
|
|||||||
|
|
||||||
RouteRequests.Remove(destination.Key);
|
RouteRequests.Remove(destination.Key);
|
||||||
|
|
||||||
var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination);
|
var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination);
|
||||||
if (current != null)
|
if (current != null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Releasing current route: {0}", destination, current.Source.Key);
|
Debug.LogMessage(LogEventLevel.Debug, "Releasing current route: {0}", destination, current.Source.Key);
|
||||||
current.ReleaseRoutes();
|
current.ReleaseRoutes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a RouteDescriptor that contains the steps necessary to make a route between devices.
|
/// Builds a RouteDescriptor that contains the steps necessary to make a route between devices.
|
||||||
/// Routes of type AudioVideo will be built as two separate routes, audio and video. If
|
/// Routes of type AudioVideo will be built as two separate routes, audio and video. If
|
||||||
/// a route is discovered, a new RouteDescriptor is returned. If one or both parts
|
/// a route is discovered, a new RouteDescriptor is returned. If one or both parts
|
||||||
/// of an audio/video route are discovered a route descriptor is returned. If no route is
|
/// of an audio/video route are discovered a route descriptor is returned. If no route is
|
||||||
/// discovered, then null is returned
|
/// discovered, then null is returned
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static RouteDescriptor GetRouteToSource(this IRoutingSink destination, IRoutingOutputs source, eRoutingSignalType signalType)
|
public static RouteDescriptor GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort, RoutingOutputPort sourcePort)
|
||||||
{
|
{
|
||||||
var routeDescriptor = new RouteDescriptor(source, destination, signalType);
|
var routeDescriptor = new RouteDescriptor(source, destination, signalType);
|
||||||
|
|
||||||
// if it's a single signal type, find the route
|
// if it's a single signal type, find the route
|
||||||
if (!signalType.HasFlag(eRoutingSignalType.AudioVideo))
|
if (!signalType.HasFlag(eRoutingSignalType.AudioVideo))
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {0}", null, source.Key);
|
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {0}", null, source.Key);
|
||||||
|
|
||||||
if (!destination.GetRouteToSource(source, null, null, signalType, 0, routeDescriptor))
|
if (!destination.GetRouteToSource(source, sourcePort, null, signalType, 0, routeDescriptor, destinationPort))
|
||||||
routeDescriptor = null;
|
routeDescriptor = null;
|
||||||
|
|
||||||
return routeDescriptor;
|
return routeDescriptor;
|
||||||
}
|
}
|
||||||
// otherwise, audioVideo needs to be handled as two steps.
|
// otherwise, audioVideo needs to be handled as two steps.
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build audio and video routes from {0}", destination, source.Key);
|
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build audio and video routes from {0}", destination, source.Key);
|
||||||
|
|
||||||
var audioSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Audio, 0, routeDescriptor);
|
var audioSuccess = destination.GetRouteToSource(source, sourcePort, null, eRoutingSignalType.Audio, 0, routeDescriptor, destinationPort);
|
||||||
|
|
||||||
if (!audioSuccess)
|
if (!audioSuccess)
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key);
|
Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key);
|
||||||
|
|
||||||
var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, routeDescriptor);
|
var videoSuccess = destination.GetRouteToSource(source, sourcePort, null, eRoutingSignalType.Video, 0, routeDescriptor, destinationPort);
|
||||||
|
|
||||||
if (!videoSuccess)
|
if (!videoSuccess)
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Cannot find video route to {0}", destination, source.Key);
|
Debug.LogMessage(LogEventLevel.Debug, "Cannot find video route to {0}", destination, source.Key);
|
||||||
|
|
||||||
if (!audioSuccess && !videoSuccess)
|
if (!audioSuccess && !videoSuccess)
|
||||||
routeDescriptor = null;
|
routeDescriptor = null;
|
||||||
|
|
||||||
|
|
||||||
return routeDescriptor;
|
return routeDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The recursive part of this. Will stop on each device, search its inputs for the
|
/// The recursive part of this. Will stop on each device, search its inputs for the
|
||||||
/// desired source and if not found, invoke this function for the each input port
|
/// desired source and if not found, invoke this function for the each input port
|
||||||
/// hoping to find the source.
|
/// hoping to find the source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="destination"></param>
|
/// <param name="destination"></param>
|
||||||
/// <param name="source"></param>
|
/// <param name="source"></param>
|
||||||
/// <param name="outputPortToUse">The RoutingOutputPort whose link is being checked for a route</param>
|
/// <param name="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
|
||||||
/// <param name="alreadyCheckedDevices">Prevents Devices from being twice-checked</param>
|
/// <param name="alreadyCheckedDevices">Prevents Devices from being twice-checked</param>
|
||||||
/// <param name="signalType">This recursive function should not be called with AudioVideo</param>
|
/// <param name="signalType">This recursive function should not be called with AudioVideo</param>
|
||||||
/// <param name="cycle">Just an informational counter</param>
|
/// <param name="cycle">Just an informational counter</param>
|
||||||
/// <param name="routeTable">The RouteDescriptor being populated as the route is discovered</param>
|
/// <param name="routeTable">The RouteDescriptor being populated as the route is discovered</param>
|
||||||
/// <returns>true if source is hit</returns>
|
/// <returns>true if source is hit</returns>
|
||||||
static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
|
private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
|
||||||
RoutingOutputPort outputPortToUse, List<IRoutingInputsOutputs> alreadyCheckedDevices,
|
RoutingOutputPort sourcePort, List<IRoutingInputsOutputs> alreadyCheckedDevices,
|
||||||
eRoutingSignalType signalType, int cycle, RouteDescriptor routeTable)
|
eRoutingSignalType signalType, int cycle, RouteDescriptor routeTable, RoutingInputPort destinationPort)
|
||||||
{
|
{
|
||||||
cycle++;
|
cycle++;
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "GetRouteToSource: {0} {1}--> {2}", null, cycle, source.Key, destination.Key);
|
Debug.LogMessage(LogEventLevel.Verbose, "GetRouteToSource: {0} {1}--> {2}", null, cycle, source.Key, destination.Key);
|
||||||
|
|
||||||
RoutingInputPort goodInputPort = null;
|
RoutingInputPort goodInputPort = null;
|
||||||
|
|
||||||
var destinationTieLines = TieLineCollection.Default.Where(t =>
|
IEnumerable<TieLine> destinationTieLines;
|
||||||
t.DestinationPort.ParentDevice == destination && (t.Type == signalType || t.Type.HasFlag(eRoutingSignalType.AudioVideo)));
|
TieLine directTie = null;
|
||||||
|
|
||||||
// find a direct tie
|
if (destinationPort == null)
|
||||||
var directTie = destinationTieLines.FirstOrDefault(
|
{
|
||||||
t => t.DestinationPort.ParentDevice == destination
|
|
||||||
&& t.SourcePort.ParentDevice == source);
|
|
||||||
if (directTie != null) // Found a tie directly to the source
|
|
||||||
{
|
|
||||||
goodInputPort = directTie.DestinationPort;
|
|
||||||
}
|
|
||||||
else // no direct-connect. Walk back devices.
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "is not directly connected to {0}. Walking down tie lines", destination, source.Key);
|
|
||||||
|
|
||||||
// No direct tie? Run back out on the inputs' attached devices...
|
destinationTieLines = TieLineCollection.Default.Where(t =>
|
||||||
// Only the ones that are routing devices
|
t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type == signalType || t.Type.HasFlag(eRoutingSignalType.AudioVideo)));
|
||||||
var attachedMidpoints = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destinationTieLines = TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type == signalType || t.Type.HasFlag(eRoutingSignalType.AudioVideo)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the TieLine without a port
|
||||||
|
if (destinationPort == null && sourcePort == null)
|
||||||
|
{
|
||||||
|
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key);
|
||||||
|
}
|
||||||
|
// find a tieLine to a specific destination port without a specific source port
|
||||||
|
else if (destinationPort != null && sourcePort == null)
|
||||||
|
{
|
||||||
|
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key);
|
||||||
|
}
|
||||||
|
// find a tieline to a specific source port without a specific destination port
|
||||||
|
else if (destinationPort == null & sourcePort != null)
|
||||||
|
{
|
||||||
|
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.Key == sourcePort.Key);
|
||||||
|
}
|
||||||
|
// find a tieline to a specific source port and destination port
|
||||||
|
else if (destinationPort != null && sourcePort != null)
|
||||||
|
{
|
||||||
|
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.Key == sourcePort.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directTie != null) // Found a tie directly to the source
|
||||||
|
{
|
||||||
|
goodInputPort = directTie.DestinationPort;
|
||||||
|
}
|
||||||
|
else // no direct-connect. Walk back devices.
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Verbose, "is not directly connected to {0}. Walking down tie lines", destination, source.Key);
|
||||||
|
|
||||||
|
// No direct tie? Run back out on the inputs' attached devices...
|
||||||
|
// Only the ones that are routing devices
|
||||||
|
var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs);
|
||||||
|
|
||||||
//Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration
|
//Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration
|
||||||
if (alreadyCheckedDevices == null)
|
if (alreadyCheckedDevices == null)
|
||||||
alreadyCheckedDevices = new List<IRoutingInputsOutputs>();
|
alreadyCheckedDevices = new List<IRoutingInputsOutputs>();
|
||||||
alreadyCheckedDevices.Add(destination as IRoutingInputsOutputs);
|
alreadyCheckedDevices.Add(destination as IRoutingInputsOutputs);
|
||||||
|
|
||||||
foreach (var inputTieToTry in attachedMidpoints)
|
foreach (var tieLine in midpointTieLines)
|
||||||
{
|
{
|
||||||
var upstreamDeviceOutputPort = inputTieToTry.SourcePort;
|
var midpointDevice = tieLine.SourcePort.ParentDevice as IRoutingInputsOutputs;
|
||||||
var upstreamRoutingDevice = upstreamDeviceOutputPort.ParentDevice as IRoutingInputsOutputs;
|
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Trying to find route on {0}", destination, upstreamRoutingDevice.Key);
|
|
||||||
|
|
||||||
// Check if this previous device has already been walked
|
// Check if this previous device has already been walked
|
||||||
if (alreadyCheckedDevices.Contains(upstreamRoutingDevice))
|
if (alreadyCheckedDevices.Contains(midpointDevice))
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Skipping input {0} on {1}, this was already checked", destination, upstreamRoutingDevice.Key, destination.Key);
|
Debug.LogMessage(LogEventLevel.Verbose, "Skipping input {0} on {1}, this was already checked", destination, midpointDevice.Key, destination.Key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var midpointOutputPort = sourcePort ?? tieLine.SourcePort;
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Verbose, "Trying to find route on {0}", destination, midpointDevice.Key);
|
||||||
|
|
||||||
// haven't seen this device yet. Do it. Pass the output port to the next
|
// haven't seen this device yet. Do it. Pass the output port to the next
|
||||||
// level to enable switching on success
|
// level to enable switching on success
|
||||||
var upstreamRoutingSuccess = upstreamRoutingDevice.GetRouteToSource(source, upstreamDeviceOutputPort,
|
var upstreamRoutingSuccess = midpointDevice.GetRouteToSource(source, midpointOutputPort,
|
||||||
alreadyCheckedDevices, signalType, cycle, routeTable);
|
alreadyCheckedDevices, signalType, cycle, routeTable, null);
|
||||||
|
|
||||||
if (upstreamRoutingSuccess)
|
if (upstreamRoutingSuccess)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Upstream device route found", destination);
|
Debug.LogMessage(LogEventLevel.Verbose, "Upstream device route found", destination);
|
||||||
goodInputPort = inputTieToTry.DestinationPort;
|
goodInputPort = tieLine.DestinationPort;
|
||||||
break; // Stop looping the inputs in this cycle
|
break; // Stop looping the inputs in this cycle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (goodInputPort == null)
|
if (goodInputPort == null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key);
|
Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we have a route on corresponding inputPort. *** Do the route ***
|
// we have a route on corresponding inputPort. *** Do the route ***
|
||||||
|
|
||||||
if (outputPortToUse == null)
|
if (sourcePort == null)
|
||||||
{
|
{
|
||||||
// it's a sink device
|
// it's a sink device
|
||||||
routeTable.Routes.Add(new RouteSwitchDescriptor(goodInputPort));
|
routeTable.Routes.Add(new RouteSwitchDescriptor(goodInputPort));
|
||||||
}
|
}
|
||||||
else if (destination is IRouting)
|
else if (destination is IRouting)
|
||||||
{
|
{
|
||||||
routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort));
|
routeTable.Routes.Add(new RouteSwitchDescriptor(sourcePort, goodInputPort));
|
||||||
}
|
}
|
||||||
else // device is merely IRoutingInputOutputs
|
else // device is merely IRoutingInputOutputs
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "No routing. Passthrough device", destination);
|
Debug.LogMessage(LogEventLevel.Verbose, "No routing. Passthrough device", destination);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines an IRoutingOutputs devices as being a source - the start of the chain
|
/// Defines an IRoutingOutputs devices as being a source - the start of the chain
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IRoutingSource
|
public interface IRoutingSource : IRoutingOutputs
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,19 +2,22 @@
|
|||||||
{
|
{
|
||||||
public class RouteRequest
|
public class RouteRequest
|
||||||
{
|
{
|
||||||
public IRoutingSink Destination {get; set;}
|
public RoutingInputPort DestinationPort { get; set; }
|
||||||
public IRoutingOutputs Source {get; set;}
|
|
||||||
public eRoutingSignalType SignalType {get; set;}
|
public RoutingOutputPort SourcePort { get; set; }
|
||||||
|
public IRoutingInputs Destination { get; set; }
|
||||||
|
public IRoutingOutputs Source { get; set; }
|
||||||
|
public eRoutingSignalType SignalType { get; set; }
|
||||||
|
|
||||||
public void HandleCooldown(object sender, FeedbackEventArgs args)
|
public void HandleCooldown(object sender, FeedbackEventArgs args)
|
||||||
{
|
{
|
||||||
var coolingDevice = sender as IWarmingCooling;
|
var coolingDevice = sender as IWarmingCooling;
|
||||||
|
|
||||||
if(args.BoolValue == false)
|
if (args.BoolValue == false)
|
||||||
{
|
{
|
||||||
Destination.ReleaseAndMakeRoute(Source, SignalType);
|
Destination.ReleaseAndMakeRoute(Source, SignalType);
|
||||||
|
|
||||||
if(sender == null) return;
|
if (sender == null) return;
|
||||||
|
|
||||||
coolingDevice.IsCoolingDownFeedback.OutputChange -= HandleCooldown;
|
coolingDevice.IsCoolingDownFeedback.OutputChange -= HandleCooldown;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user