diff --git a/src/Aeon.Foraging/Aeon.Foraging.csproj b/src/Aeon.Foraging/Aeon.Foraging.csproj
index 013a26f..cb386c4 100644
--- a/src/Aeon.Foraging/Aeon.Foraging.csproj
+++ b/src/Aeon.Foraging/Aeon.Foraging.csproj
@@ -7,7 +7,7 @@
Bonsai Rx Project Aeon Foraging
net472
0.1.0
- build231009
+ build231010
diff --git a/src/Aeon.Foraging/Aeon.Foraging.csproj.user b/src/Aeon.Foraging/Aeon.Foraging.csproj.user
index 9c54aa0..746aabd 100644
--- a/src/Aeon.Foraging/Aeon.Foraging.csproj.user
+++ b/src/Aeon.Foraging/Aeon.Foraging.csproj.user
@@ -2,7 +2,7 @@
-
+
UserControl
diff --git a/src/Aeon.Foraging/CreateDispenserEvent.cs b/src/Aeon.Foraging/CreateDispenserEvent.cs
new file mode 100644
index 0000000..e6f9176
--- /dev/null
+++ b/src/Aeon.Foraging/CreateDispenserEvent.cs
@@ -0,0 +1,28 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+using Bonsai;
+
+namespace Aeon.Foraging
+{
+ [Description("Create a sequence of dispenser event commands.")]
+ public class CreateDispenserEvent : Source
+ {
+ [Description("The number of dispenser units associated with the event command.")]
+ public int Value { get; set; }
+
+ [Description("Specifies the type of dispenser event command to create.")]
+ public DispenserEventType EventType { get; set; }
+
+ public override IObservable Generate()
+ {
+ return Observable.Return(new DispenserEventArgs(Value, EventType));
+ }
+
+ public IObservable Generate(IObservable source)
+ {
+ return source.Select(value => new DispenserEventArgs(Value, EventType));
+ }
+ }
+}
diff --git a/src/Aeon.Foraging/DispenserAccumulate.cs b/src/Aeon.Foraging/DispenserAccumulate.cs
new file mode 100644
index 0000000..c008f0e
--- /dev/null
+++ b/src/Aeon.Foraging/DispenserAccumulate.cs
@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+using Bonsai;
+
+namespace Aeon.Foraging
+{
+ [Combinator]
+ [Description("Generates a sequence of the estimated number of units in the specified dispenser.")]
+ public class DispenserAccumulate
+ {
+ public IObservable Process(IObservable source)
+ {
+ return source.Scan(new DispenserState(), Accumulate);
+ }
+
+ public IObservable Process(IObservable source, IObservable seed)
+ {
+ return seed.Take(1).SelectMany(state => source.Scan(state, Accumulate));
+ }
+
+ static DispenserState Accumulate(DispenserState state, DispenserEventArgs evt)
+ {
+ return evt.EventType switch
+ {
+ DispenserEventType.Discount => new DispenserState { Count = state.Count - evt.Value },
+ DispenserEventType.Refill => new DispenserState { Count = state.Count + evt.Value },
+ DispenserEventType.Reset => new DispenserState { Count = evt.Value },
+ _ => throw new InvalidOperationException("Invalid dispenser event type."),
+ };
+ }
+ }
+
+ public class DispenserState
+ {
+ public int Count { get; set; }
+
+ public override string ToString()
+ {
+ return $"DispenserState(Total: {Count})";
+ }
+ }
+}
diff --git a/src/Aeon.Foraging/DispenserController.cs b/src/Aeon.Foraging/DispenserController.cs
new file mode 100644
index 0000000..b687454
--- /dev/null
+++ b/src/Aeon.Foraging/DispenserController.cs
@@ -0,0 +1,37 @@
+using Aeon.Acquisition;
+using Bonsai;
+using Bonsai.Harp;
+using System;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+
+namespace Aeon.Foraging
+{
+ [DefaultProperty(nameof(Name))]
+ [TypeVisualizer(typeof(DispenserEventVisualizer))]
+ [Description("Generates a sequence of event commands for the specified dispenser.")]
+ public class DispenserController : MetadataSource, INamedElement
+ {
+ [Description("The name of the dispenser.")]
+ public string Name { get; set; }
+
+ string INamedElement.Name => $"{Name}{nameof(DispenserController)}";
+
+ internal BehaviorSubject State { get; } = new(value: default);
+
+ public IObservable Process(IObservable source)
+ {
+ return Process().Merge(source
+ .Do(State).IgnoreElements()
+ .Cast());
+ }
+
+ public IObservable> Process(IObservable source, IObservable clockSource)
+ {
+ return Process(clockSource).Merge(source
+ .Do(State).IgnoreElements()
+ .Cast>());
+ }
+ }
+}
diff --git a/src/Aeon.Foraging/DispenserStateMetadata.cs b/src/Aeon.Foraging/DispenserEventArgs.cs
similarity index 58%
rename from src/Aeon.Foraging/DispenserStateMetadata.cs
rename to src/Aeon.Foraging/DispenserEventArgs.cs
index d14e5e7..6d5aff9 100644
--- a/src/Aeon.Foraging/DispenserStateMetadata.cs
+++ b/src/Aeon.Foraging/DispenserEventArgs.cs
@@ -1,23 +1,20 @@
namespace Aeon.Foraging
{
- public class DispenserStateMetadata
+ public class DispenserEventArgs
{
- public DispenserStateMetadata(string name, int value, DispenserEventType eventType)
+ public DispenserEventArgs(int value, DispenserEventType eventType)
{
- Name = name;
Value = value;
EventType = eventType;
}
- public string Name { get; }
-
public int Value { get; }
public DispenserEventType EventType { get; }
public override string ToString()
{
- return $"DispenserState({Name}, {EventType}, Total:{Value})";
+ return $"DispenserEvent({EventType}, Value:{Value})";
}
}
diff --git a/src/Aeon.Foraging/DispenserStateControl.Designer.cs b/src/Aeon.Foraging/DispenserEventControl.Designer.cs
similarity index 82%
rename from src/Aeon.Foraging/DispenserStateControl.Designer.cs
rename to src/Aeon.Foraging/DispenserEventControl.Designer.cs
index 755d965..017a26c 100644
--- a/src/Aeon.Foraging/DispenserStateControl.Designer.cs
+++ b/src/Aeon.Foraging/DispenserEventControl.Designer.cs
@@ -1,6 +1,6 @@
namespace Aeon.Foraging
{
- partial class DispenserStateControl
+ partial class DispenserEventControl
{
///
/// Required designer variable.
@@ -29,12 +29,13 @@ protected override void Dispose(bool disposing)
private void InitializeComponent()
{
this.dispenserPanel = new System.Windows.Forms.Panel();
+ this.deliverButton = new System.Windows.Forms.Button();
+ this.resetButton = new System.Windows.Forms.Button();
this.dispenserGroupBox = new System.Windows.Forms.GroupBox();
this.currentValueLabel = new System.Windows.Forms.Label();
this.currentLabel = new System.Windows.Forms.Label();
this.refillUpDown = new System.Windows.Forms.NumericUpDown();
this.refillButton = new System.Windows.Forms.Button();
- this.resetButton = new System.Windows.Forms.Button();
this.dispenserPanel.SuspendLayout();
this.dispenserGroupBox.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.refillUpDown)).BeginInit();
@@ -42,6 +43,7 @@ private void InitializeComponent()
//
// dispenserPanel
//
+ this.dispenserPanel.Controls.Add(this.deliverButton);
this.dispenserPanel.Controls.Add(this.resetButton);
this.dispenserPanel.Controls.Add(this.dispenserGroupBox);
this.dispenserPanel.Controls.Add(this.refillButton);
@@ -49,13 +51,37 @@ private void InitializeComponent()
this.dispenserPanel.Location = new System.Drawing.Point(0, 0);
this.dispenserPanel.Margin = new System.Windows.Forms.Padding(6);
this.dispenserPanel.Name = "dispenserPanel";
- this.dispenserPanel.Size = new System.Drawing.Size(400, 240);
+ this.dispenserPanel.Size = new System.Drawing.Size(400, 180);
this.dispenserPanel.TabIndex = 10;
//
- // dispenserGroupBox
+ // deliverButton
+ //
+ this.deliverButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.deliverButton.Location = new System.Drawing.Point(213, 23);
+ this.deliverButton.Margin = new System.Windows.Forms.Padding(12);
+ this.deliverButton.Name = "deliverButton";
+ this.deliverButton.Size = new System.Drawing.Size(175, 40);
+ this.deliverButton.TabIndex = 7;
+ this.deliverButton.Text = "Deliver";
+ this.deliverButton.UseVisualStyleBackColor = true;
+ this.deliverButton.Click += new System.EventHandler(this.deliverButton_Click);
+ //
+ // resetButton
//
- this.dispenserGroupBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
+ this.resetButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
+ this.resetButton.Location = new System.Drawing.Point(214, 125);
+ this.resetButton.Margin = new System.Windows.Forms.Padding(12);
+ this.resetButton.Name = "resetButton";
+ this.resetButton.Size = new System.Drawing.Size(175, 40);
+ this.resetButton.TabIndex = 6;
+ this.resetButton.Text = "Reset";
+ this.resetButton.UseVisualStyleBackColor = true;
+ this.resetButton.Click += new System.EventHandler(this.resetButton_Click);
+ //
+ // dispenserGroupBox
+ //
this.dispenserGroupBox.Controls.Add(this.currentValueLabel);
this.dispenserGroupBox.Controls.Add(this.currentLabel);
this.dispenserGroupBox.Controls.Add(this.refillUpDown);
@@ -63,7 +89,7 @@ private void InitializeComponent()
this.dispenserGroupBox.Margin = new System.Windows.Forms.Padding(12);
this.dispenserGroupBox.Name = "dispenserGroupBox";
this.dispenserGroupBox.Padding = new System.Windows.Forms.Padding(12);
- this.dispenserGroupBox.Size = new System.Drawing.Size(375, 153);
+ this.dispenserGroupBox.Size = new System.Drawing.Size(197, 153);
this.dispenserGroupBox.TabIndex = 4;
this.dispenserGroupBox.TabStop = false;
this.dispenserGroupBox.Text = "Dispenser";
@@ -75,7 +101,7 @@ private void InitializeComponent()
this.currentValueLabel.AutoSize = true;
this.currentValueLabel.Location = new System.Drawing.Point(145, 43);
this.currentValueLabel.Name = "currentValueLabel";
- this.currentValueLabel.Size = new System.Drawing.Size(31, 32);
+ this.currentValueLabel.Size = new System.Drawing.Size(24, 26);
this.currentValueLabel.TabIndex = 2;
this.currentValueLabel.Text = "0";
//
@@ -84,7 +110,7 @@ private void InitializeComponent()
this.currentLabel.AutoSize = true;
this.currentLabel.Location = new System.Drawing.Point(15, 43);
this.currentLabel.Name = "currentLabel";
- this.currentLabel.Size = new System.Drawing.Size(124, 32);
+ this.currentLabel.Size = new System.Drawing.Size(96, 26);
this.currentLabel.TabIndex = 1;
this.currentLabel.Text = "Current: ";
//
@@ -99,45 +125,31 @@ private void InitializeComponent()
0,
0});
this.refillUpDown.Name = "refillUpDown";
- this.refillUpDown.Size = new System.Drawing.Size(345, 38);
+ this.refillUpDown.Size = new System.Drawing.Size(167, 32);
this.refillUpDown.TabIndex = 0;
//
// refillButton
//
- this.refillButton.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
- | System.Windows.Forms.AnchorStyles.Left)
+ this.refillButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
- this.refillButton.Location = new System.Drawing.Point(12, 175);
+ this.refillButton.Location = new System.Drawing.Point(214, 75);
this.refillButton.Margin = new System.Windows.Forms.Padding(12);
this.refillButton.Name = "refillButton";
- this.refillButton.Size = new System.Drawing.Size(176, 52);
+ this.refillButton.Size = new System.Drawing.Size(175, 40);
this.refillButton.TabIndex = 5;
this.refillButton.Text = "Refill";
this.refillButton.UseVisualStyleBackColor = true;
this.refillButton.Click += new System.EventHandler(this.refillButton_Click);
//
- // resetButton
- //
- this.resetButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.resetButton.Location = new System.Drawing.Point(212, 175);
- this.resetButton.Margin = new System.Windows.Forms.Padding(12);
- this.resetButton.Name = "resetButton";
- this.resetButton.Size = new System.Drawing.Size(175, 52);
- this.resetButton.TabIndex = 6;
- this.resetButton.Text = "Reset";
- this.resetButton.UseVisualStyleBackColor = true;
- this.resetButton.Click += new System.EventHandler(this.resetButton_Click);
- //
- // DispenserStateControl
+ // DispenserEventControl
//
- this.AutoScaleDimensions = new System.Drawing.SizeF(16F, 31F);
+ this.AutoScaleDimensions = new System.Drawing.SizeF(13F, 26F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.dispenserPanel);
this.Font = new System.Drawing.Font("Microsoft Sans Serif", 16.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.Margin = new System.Windows.Forms.Padding(6);
- this.Name = "DispenserStateControl";
- this.Size = new System.Drawing.Size(400, 240);
+ this.Name = "DispenserEventControl";
+ this.Size = new System.Drawing.Size(400, 180);
this.dispenserPanel.ResumeLayout(false);
this.dispenserGroupBox.ResumeLayout(false);
this.dispenserGroupBox.PerformLayout();
@@ -155,5 +167,6 @@ private void InitializeComponent()
private System.Windows.Forms.Label currentValueLabel;
private System.Windows.Forms.Label currentLabel;
private System.Windows.Forms.Button resetButton;
+ private System.Windows.Forms.Button deliverButton;
}
}
diff --git a/src/Aeon.Foraging/DispenserStateControl.cs b/src/Aeon.Foraging/DispenserEventControl.cs
similarity index 53%
rename from src/Aeon.Foraging/DispenserStateControl.cs
rename to src/Aeon.Foraging/DispenserEventControl.cs
index af768e0..b9b1e41 100644
--- a/src/Aeon.Foraging/DispenserStateControl.cs
+++ b/src/Aeon.Foraging/DispenserEventControl.cs
@@ -3,11 +3,11 @@
namespace Aeon.Foraging
{
- public partial class DispenserStateControl : UserControl
+ public partial class DispenserEventControl : UserControl
{
int currentValue;
- public DispenserStateControl(DispenserState source)
+ public DispenserEventControl(DispenserController source)
{
Source = source ?? throw new ArgumentNullException(nameof(source));
InitializeComponent();
@@ -15,11 +15,11 @@ public DispenserStateControl(DispenserState source)
var dispenserName = Source.Name;
if (!string.IsNullOrEmpty(dispenserName))
{
- dispenserGroupBox.Text = $"{dispenserName} Dispenser";
+ dispenserGroupBox.Text = $"{dispenserName}";
}
}
- public DispenserState Source { get; }
+ public DispenserController Source { get; }
public int Value
{
@@ -31,16 +31,24 @@ public int Value
}
}
+ private void deliverButton_Click(object sender, EventArgs e)
+ {
+ OnDispenserEvent(1m, DispenserEventType.Discount);
+ }
+
private void refillButton_Click(object sender, EventArgs e)
{
- var metadata = new DispenserStateMetadata(Source.Name, (int)refillUpDown.Value, DispenserEventType.Refill);
- Source.OnNext(metadata);
+ OnDispenserEvent(refillUpDown.Value, DispenserEventType.Refill);
}
private void resetButton_Click(object sender, EventArgs e)
{
- var delta = (int)refillUpDown.Value - Value;
- var metadata = new DispenserStateMetadata(Source.Name, delta, DispenserEventType.Reset);
+ OnDispenserEvent(refillUpDown.Value, DispenserEventType.Reset);
+ }
+
+ private void OnDispenserEvent(decimal value, DispenserEventType eventType)
+ {
+ var metadata = new DispenserEventArgs((int)value, eventType);
Source.OnNext(metadata);
}
}
diff --git a/src/Aeon.Foraging/DispenserStateControl.resx b/src/Aeon.Foraging/DispenserEventControl.resx
similarity index 100%
rename from src/Aeon.Foraging/DispenserStateControl.resx
rename to src/Aeon.Foraging/DispenserEventControl.resx
diff --git a/src/Aeon.Foraging/DispenserStateVisualizer.cs b/src/Aeon.Foraging/DispenserEventVisualizer.cs
similarity index 50%
rename from src/Aeon.Foraging/DispenserStateVisualizer.cs
rename to src/Aeon.Foraging/DispenserEventVisualizer.cs
index bd658b1..f2237c3 100644
--- a/src/Aeon.Foraging/DispenserStateVisualizer.cs
+++ b/src/Aeon.Foraging/DispenserEventVisualizer.cs
@@ -1,43 +1,49 @@
using Bonsai.Design;
using Bonsai.Expressions;
-using Bonsai.Harp;
using System;
+using System.Drawing;
using System.Windows.Forms;
namespace Aeon.Foraging
{
- public class DispenserStateVisualizer : DialogTypeVisualizer
+ public class DispenserEventVisualizer : DialogTypeVisualizer
{
- DispenserStateControl control;
+ DispenserEventControl control;
+ IDisposable stateSubscription;
public override void Load(IServiceProvider provider)
{
var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext));
var visualizerElement = ExpressionBuilder.GetVisualizerElement(context.Source);
- var source = (DispenserState)ExpressionBuilder.GetWorkflowElement(visualizerElement.Builder);
+ var source = (DispenserController)ExpressionBuilder.GetWorkflowElement(visualizerElement.Builder);
- control = new DispenserStateControl(source);
+ control = new DispenserEventControl(source);
control.Dock = DockStyle.Fill;
- var state = source.State;
- if (state != null) control.Value = state.Value;
+ control.HandleDestroyed += delegate { stateSubscription?.Dispose(); };
+ control.HandleCreated += delegate
+ {
+ stateSubscription = source.State.ObserveOn(control).Subscribe(state =>
+ {
+ if (state != null)
+ {
+ control.Value = state.Count;
+ }
+ });
+ };
var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService));
if (visualizerService != null)
{
+ using var graphics = Graphics.FromHwnd(IntPtr.Zero);
+ control.Scale(new SizeF(
+ graphics.DpiX / control.DeviceDpi,
+ graphics.DpiY / control.DeviceDpi));
visualizerService.AddControl(control);
}
}
public override void Show(object value)
{
- if (value is DispenserStateMetadata metadata)
- {
- control.Value = metadata.Value;
- }
- else if (value is Timestamped timestampedMetadata)
- {
- control.Value = timestampedMetadata.Value.Value;
- }
}
public override void Unload()
diff --git a/src/Aeon.Foraging/DispenserState.cs b/src/Aeon.Foraging/DispenserState.cs
deleted file mode 100644
index 874db3b..0000000
--- a/src/Aeon.Foraging/DispenserState.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using Aeon.Acquisition;
-using Bonsai;
-using Bonsai.Harp;
-using System;
-using System.ComponentModel;
-using System.Reactive.Linq;
-
-namespace Aeon.Foraging
-{
- [DefaultProperty(nameof(Name))]
- [TypeVisualizer(typeof(DispenserStateVisualizer))]
- [Description("Generates a sequence of the estimated number of units in the specified dispenser.")]
- public class DispenserState : MetadataSource, INamedElement
- {
- [Description("The name of the dispenser.")]
- public string Name { get; set; }
-
- string INamedElement.Name => $"{Name}Dispenser";
-
- internal DispenserStateRecovery State { get; set; }
-
- public override IObservable> Process(IObservable source)
- {
- var refill = Process();
- return Observable.Defer(() =>
- {
- State = StateRecovery.Deserialize(Name);
- var initialState = new DispenserStateMetadata(Name, State.Value, DispenserEventType.Reset);
- return source.Publish(ps =>
- {
- const int DigitalOutput = 35;
- const int TriggerPellet = 0x80;
- var discount = ps.Where(message =>
- message.Address == DigitalOutput &&
- message.MessageType == MessageType.Write &&
- message.GetPayloadByte() == TriggerPellet)
- .Select(_ => new DispenserStateMetadata(Name, -1, DispenserEventType.Discount));
- return refill.Merge(discount).StartWith(initialState).Publish(changes =>
- changes.CombineLatest(ps, (data, message) => (data, message))
- .Sample(changes.MergeUnit(ps.Take(1)))
- .Select((x, i) =>
- {
- var data = x.data;
- var timestamp = x.message.GetTimestamp();
- if (i > 0) State.Value += data.Value;
- StateRecovery.Serialize(Name, State);
- data = new DispenserStateMetadata(data.Name, State.Value, data.EventType);
- return Timestamped.Create(data, timestamp);
- }));
- });
- });
- }
-
- public IObservable> Process(IObservable dispenser, IObservable source)
- {
- var refill = Process();
- var discount = dispenser.Select(x => new DispenserStateMetadata(Name, -x, DispenserEventType.Discount));
- return Observable.Defer(() =>
- {
- State = StateRecovery.Deserialize(Name);
- var initialState = new DispenserStateMetadata(Name, State.Value, DispenserEventType.Reset);
- return discount.Merge(refill).StartWith(initialState).Select((data, i) =>
- {
- if (i > 0) State.Value += data.Value;
- StateRecovery.Serialize(Name, State);
- data = new DispenserStateMetadata(data.Name, State.Value, data.EventType);
- return data;
- }).Timestamp(source);
- });
- }
- }
-
- public class DispenserStateRecovery
- {
- public int Value { get; set; }
- }
-}
diff --git a/src/Aeon.Foraging/FormatDispenserState.cs b/src/Aeon.Foraging/FormatDispenserState.cs
index c638ccc..9ce6f11 100644
--- a/src/Aeon.Foraging/FormatDispenserState.cs
+++ b/src/Aeon.Foraging/FormatDispenserState.cs
@@ -12,15 +12,16 @@ namespace Aeon.Foraging
[WorkflowElementCategory(ElementCategory.Transform)]
public class FormatDispenserState
{
- const int Address = 200;
+ [Description("The address of the virtual Harp register.")]
+ public int Address { get; set; } = 200;
- public IObservable Process(IObservable> source)
+ public IObservable Process(IObservable> source)
{
return source.Select(input => HarpMessage.FromSingle(
Address,
input.Seconds,
MessageType.Event,
- input.Value.Value));
+ input.Value.Count));
}
}
}
diff --git a/src/Aeon.Foraging/PatchDispenser.bonsai b/src/Aeon.Foraging/PatchDispenser.bonsai
new file mode 100644
index 0000000..4198202
--- /dev/null
+++ b/src/Aeon.Foraging/PatchDispenser.bonsai
@@ -0,0 +1,101 @@
+
+
+ Provides an interface for controlling patch dispensers. Input sequence represents pellet discount notifications.
+
+
+
+
+
+
+ PatchController
+
+
+ NotDiscount
+
+
+
+ Source1
+
+
+ EventType
+
+
+
+ Discount
+
+
+
+
+
+
+
+
+
+
+
+
+ Source1
+
+
+
+ 1
+ Discount
+
+
+
+
+
+
+
+
+
+ PatchState
+
+
+
+
+
+
+
+
+ PatchState
+
+
+ PatchState
+
+
+
+
+
+
+
+
+ PatchController
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file