diff --git a/MonkeyLoader/Configuration/ConfigKeyBidirectionalBinding.cs b/MonkeyLoader/Configuration/ConfigKeyBidirectionalBinding.cs
new file mode 100644
index 0000000..807f2c0
--- /dev/null
+++ b/MonkeyLoader/Configuration/ConfigKeyBidirectionalBinding.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonkeyLoader.Configuration
+{
+ ///
+ /// Represents the functionality for an ,
+ /// which propagate any changes in value in both directions
+ /// between the two associated config items.
+ ///
+ ///
+ public sealed class ConfigKeyBidirectionalBinding : IConfigKeyBidirectionalBinding
+ {
+ ///
+ public IDefiningConfigKey Owner { get; private set; } = null!;
+
+ IDefiningConfigKey IConfigKeyBidirectionalBinding.Owner => Owner;
+
+ ///
+ public IDefiningConfigKey Target { get; }
+
+ IDefiningConfigKey IConfigKeyBidirectionalBinding.Target => Target;
+
+ ///
+ /// Creates a new bidirectional binding targeting the given config item.
+ ///
+ /// The other config item to propagate changes to and from.
+ public ConfigKeyBidirectionalBinding(IDefiningConfigKey target)
+ {
+ Target = target;
+ }
+
+ ///
+ /// Adds the Changed event
+ /// listeners to propagate changes between the linked config items.
+ ///
+ /// When the binding has already been initialized or is targeted at itself.
+ ///
+ public void Initialize(IDefiningConfigKey entity)
+ {
+ if (Owner is not null)
+ throw new InvalidOperationException($"This binding targetting [{Target}] is already owned by [{Owner}]!");
+
+ if (ReferenceEquals(Target, entity))
+ throw new InvalidOperationException($"Can't bind [{Target}] to itself!");
+
+ Owner = entity;
+
+ // Shouldn't need circular check because Changed event is only fired for actual changes
+ Owner.Changed += (_, args) => Target.SetValue(args.NewValue!, args.GetPropagatedEventLabel(ConfigKeyBindings.SetFromBidirectionalOwnerEventLabel));
+ Target.Changed += (_, args) => Owner.SetValue(args.NewValue!, args.GetPropagatedEventLabel(ConfigKeyBindings.SetFromBidirectionalTargetEventLabel));
+ }
+ }
+
+ /// The type of the config item's value.
+ ///
+ public interface IConfigKeyBidirectionalBinding : IConfigKeyComponent>, IConfigKeyBidirectionalBinding
+ {
+ ///
+ public new IDefiningConfigKey Owner { get; }
+
+ ///
+ public new IDefiningConfigKey Target { get; }
+ }
+
+ ///
+ /// Defines the interface for config key components,
+ /// which propagate any changes in value in both directions
+ /// between the two associated config items.
+ ///
+ public interface IConfigKeyBidirectionalBinding
+ {
+ ///
+ /// Gets the config item that this component was initialized to.
+ ///
+ public IDefiningConfigKey Owner { get; }
+
+ ///
+ /// Gets the config item that this binding targets.
+ ///
+ public IDefiningConfigKey Target { get; }
+ }
+}
\ No newline at end of file
diff --git a/MonkeyLoader/Configuration/ConfigKeyBindings.cs b/MonkeyLoader/Configuration/ConfigKeyBindings.cs
new file mode 100644
index 0000000..7502f52
--- /dev/null
+++ b/MonkeyLoader/Configuration/ConfigKeyBindings.cs
@@ -0,0 +1,47 @@
+using MonkeyLoader.Components;
+using MonkeyLoader.Meta;
+
+namespace MonkeyLoader.Configuration
+{
+ ///
+ /// Contains constants and extensions for .
+ ///
+ public static class ConfigKeyBindings
+ {
+ ///
+ /// The base event label used when a config item's value is set from a
+ /// 's changed value being propagated.
+ ///
+ ///
+ /// The actual event label will have the format:
+ /// BidirectionalBindingOwner:TriggerFullId:TriggerLabel.
+ ///
+ public const string SetFromBidirectionalOwnerEventLabel = "BidirectionalBindingOwner";
+
+ ///
+ /// The base event label used when a config item's value is set from a
+ /// 's changed value being propagated.
+ ///
+ ///
+ /// The actual event label will have the format:
+ /// BidirectionalBindingTarget:TriggerFullId:TriggerLabel.
+ ///
+ public const string SetFromBidirectionalTargetEventLabel = "BidirectionalBindingTarget";
+
+ ///
+ /// Creates a new bidirectional binding that propagates changes between the two config items.
+ ///
+ /// The type of the config item's value.
+ /// The config item that the component will be initialized to.
+ /// The config item to propagate changes to and from.
+ /// The newly created bidirectional binding component.
+ public static IConfigKeyBidirectionalBinding BindBidirectionallyTo(this IDefiningConfigKey owner, IDefiningConfigKey target)
+ {
+ var binding = new ConfigKeyBidirectionalBinding(target);
+
+ owner.Add(binding);
+
+ return binding;
+ }
+ }
+}
\ No newline at end of file
diff --git a/MonkeyLoader/Configuration/ConfigSystemExtensions.cs b/MonkeyLoader/Configuration/ConfigSystemExtensions.cs
new file mode 100644
index 0000000..6fa9b13
--- /dev/null
+++ b/MonkeyLoader/Configuration/ConfigSystemExtensions.cs
@@ -0,0 +1,25 @@
+using MonkeyLoader.Meta;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonkeyLoader.Configuration
+{
+ ///
+ /// Contains extensions methods related to the config system.
+ ///
+ public static class ConfigSystemExtensions
+ {
+ ///
+ /// Gets the Label for
+ /// a propagated Changed event.
+ /// The created event label will have this format:
+ /// $"{}:{.Key.FullId}:{.Label}".
+ ///
+ /// The changed event that triggered this propagation.
+ /// The new base label for the event.
+ /// The formatted label for the propagated event.
+ public static string GetPropagatedEventLabel(this IConfigKeyChangedEventArgs changedEventArgs, string baseLabel)
+ => $"{baseLabel}:{changedEventArgs.Key.FullId}:{changedEventArgs.Label}";
+ }
+}
\ No newline at end of file
diff --git a/MonkeyLoader/Configuration/DefiningConfigKey.cs b/MonkeyLoader/Configuration/DefiningConfigKey.cs
index 00d05bf..50f1ac2 100644
--- a/MonkeyLoader/Configuration/DefiningConfigKey.cs
+++ b/MonkeyLoader/Configuration/DefiningConfigKey.cs
@@ -233,6 +233,9 @@ void IDefiningConfigKey.SetValue(object? value, string? eventLabel)
throw new ArgumentException($"Tried to set key [{Id}] to invalid value!", nameof(value));
}
+ ///
+ public override string ToString() => $"{Id} ({(_value is null ? "null" : _value.ToString())})";
+
///
bool IDefiningConfigKey.TryComputeDefault(out object? defaultValue)
{