Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Makes ConfigKeySessionShare components more resilient against deletion of their Slot and add conversion layer #58

Merged
merged 1 commit into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 147 additions & 33 deletions MonkeyLoader.Resonite.Integration/Configuration/ConfigKeySessionShare.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using FrooxEngine;
using Elements.Core;
using FrooxEngine;
using MonkeyLoader.Components;
using MonkeyLoader.Configuration;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
Expand All @@ -12,22 +14,28 @@ namespace MonkeyLoader.Resonite.Configuration
{
/// <summary>
/// Represents a wrapper for an <see cref="IDefiningConfigKey{T}"/>,
/// which makes its local value available as a shared resource in Resonite sessions.<br/>
/// Optionally allows writing back changes from the session to the config item.
/// which makes its local value available as a (converted) shared resource in Resonite <see cref="World"/>s.<br/>
/// Optionally allows writing back changes from the <see cref="World"/> to the config item.
/// </summary>
/// <typeparam name="T">The type of the config item's value.</typeparam>
public sealed class ConfigKeySessionShare<T> : IConfigKeySessionShare<T>
/// <typeparam name="TKey">The type of the config item's value.</typeparam>
/// <typeparam name="TShared">
/// The type of the resource shared in Resonite <see cref="World"/>s.
/// Must be a valid generic parameter for <see cref="ValueField{T}"/> components.
/// </typeparam>
public class ConfigKeySessionShare<TKey, TShared> : IConfigKeySessionShare<TKey, TShared>
{
private readonly Converter<TShared?, TKey?> _convertToKey;
private readonly Converter<TKey?, TShared?> _convertToShared;
private readonly Lazy<string> _sharedId;
private readonly Lazy<string> _variableName;
private IDefiningConfigKey<T> _configKey = null!;
private T? _defaultValue;
private IDefiningConfigKey<TKey> _configKey = null!;
private TShared? _defaultValue;

/// <inheritdoc/>
public bool AllowWriteBack { get; set; }

/// <inheritdoc/>
public T? DefaultValue
public TShared? DefaultValue
{
get => _defaultValue;
set
Expand All @@ -45,7 +53,7 @@ public T? DefaultValue
object? IConfigKeySessionShare.DefaultValue
{
get => DefaultValue;
set => DefaultValue = (T?)value;
set => DefaultValue = (TShared)value!;
}

/// <inheritdoc/>
Expand All @@ -59,17 +67,40 @@ public T? DefaultValue
/// which makes its config key's local value available as a shared resource in Resonite sessions.<br/>
/// Optionally allows writing back changes from the session to the config item.
/// </summary>
/// <param name="defaultValue">The default value for the shared config item for users that don't have it themselves.</param>
/// <param name="convertToShared">Converts the config item's value to the shared resource's.</param>
/// <param name="convertToKey">Converts the shared resource's value to the config item's.</param>
/// <param name="defaultValue">
/// The default value for the shared config item for users that don't have it themselves.<br/>
/// Gets converted to <typeparamref name="TShared"/> using <paramref name="convertToShared"/>.
/// </param>
/// <param name="allowWriteBack">Whether to allow writing back changes from the session to the config item.</param>
public ConfigKeySessionShare(T? defaultValue = default, bool allowWriteBack = false)
/// <exception cref="InvalidOperationException">When <typeparamref name="TShared"/> is an invalid generic argument for <see cref="ValueField{T}"/> components.</exception>
public ConfigKeySessionShare(Converter<TKey?, TShared?> convertToShared, Converter<TShared?, TKey?> convertToKey,
TKey? defaultValue = default, bool allowWriteBack = false)
{
_defaultValue = defaultValue;
if (!typeof(ValueField<TShared>).IsValidGenericType(true))
throw new InvalidOperationException("TShared must be a valid generic argument for ValueField<T> components!");

_convertToShared = convertToShared;
_convertToKey = convertToKey;

_defaultValue = convertToShared(defaultValue);
AllowWriteBack = allowWriteBack;

_sharedId = new(() => $"{SharedConfig.Identifier}.{_configKey!.FullId}");
_variableName = new(() => $"World/{SharedId}");
}

/// <inheritdoc/>
public TKey? ConvertToKey(TShared? sharedValue) => _convertToKey(sharedValue);

object? IConfigKeySessionShare.ConvertToKey(object? sharedValue) => _convertToKey((TShared?)sharedValue);

/// <inheritdoc/>
public TShared? ConvertToShared(TKey? keyValue) => _convertToShared(keyValue);

object? IConfigKeySessionShare.ConvertToShared(object? keyValue) => _convertToShared((TKey?)keyValue);

/// <summary>
/// Creates a <see cref="ValueCopy{T}"/> on the given <paramref name="field"/>'s
/// parent <see cref="Slot"/>, which drives it from the shared value.
Expand All @@ -81,7 +112,7 @@ public ConfigKeySessionShare(T? defaultValue = default, bool allowWriteBack = fa
/// </param>
/// <returns>The created <see cref="ValueCopy{T}"/> component.</returns>
/// <exception cref="InvalidOperationException">When <paramref name="writeBack"/> is <c>true</c> and <see cref="AllowWriteBack">AllowWriteBack</see> isn't.</exception>
public ValueCopy<T> Drive(IField<T> field, bool writeBack = false)
public ValueCopy<TShared> Drive(IField<TShared> field, bool writeBack = false)
{
if (!AllowWriteBack && writeBack)
throw new InvalidOperationException("Can't enable write back on a drive if it's not enabled for the config item!");
Expand All @@ -91,12 +122,22 @@ public ValueCopy<T> Drive(IField<T> field, bool writeBack = false)

/// <summary>
/// Creates a <see cref="DynamicValueVariableDriver{T}"/> on the given <paramref name="field"/>'s
/// parent <see cref="Slot"/>, which drives it from the shared value.
/// parent <see cref="Slot"/>, which drives it from the shared value.<br/>
/// The driver's <see cref="DynamicValueVariableDriver{T}.DefaultValue">DefaultValue</see>
/// is set to the shared value's <see cref="DefaultValue">DefaultValue</see>.
/// </summary>
/// <param name="field">The field to drive with the shared value.</param>
/// <returns>The created <see cref="DynamicValueVariableDriver{T}"/> component.</returns>
public DynamicValueVariableDriver<T> DriveFromVariable(IField<T> field)
=> field.DriveFromVariable(VariableName);
public DynamicValueVariableDriver<TShared> DriveFromVariable(IField<TShared> field)
{
// Get Shared Value to ensure that the necessary components exist
GetSharedValue(field.World);

var driver = field.DriveFromVariable(VariableName);
driver.DefaultValue.Value = DefaultValue!;

return driver;
}

/// <summary>
/// Gets this shared config item's <see cref="Slot"/> under the
Expand All @@ -113,7 +154,7 @@ public IEnumerable<User> GetSharingUsers(World world)
.Select(valueOverride => valueOverride.Value.User.Target)
.Where(user => user is not null);

void IComponent<IDefiningConfigKey<T>>.Initialize(IDefiningConfigKey<T> entity)
void IComponent<IDefiningConfigKey<TKey>>.Initialize(IDefiningConfigKey<TKey> entity)
{
_configKey = entity;
entity.Changed += ValueChanged;
Expand All @@ -129,45 +170,68 @@ public void SetupOverride(World world)
public void ShutdownOverride(World world)
=> world.RunSynchronously(() => GetSharedValue(world).Value.OnValueChange -= SharedValueChanged);

private ValueUserOverride<T> GetSharedOverride(World world)
private ValueUserOverride<TShared> GetSharedOverride(World world)
=> GetSharedValue(world).Value.GetUserOverride();

private ValueField<T> GetSharedValue(World world)
=> world.GetSharedComponentOrCreate<ValueField<T>>(SharedId, SetupSharedField, 0, true, false, () => GetSharedConfigSlot(world));
private ValueField<TShared> GetSharedValue(World world)
=> world.GetSharedComponentOrCreate<ValueField<TShared>>(SharedId, SetupSharedField, 0, true, false, () => GetSharedConfigSlot(world));

private void SetupSharedField(ValueField<T> field)
private void SetupSharedField(ValueField<TShared> field)
{
if (!field.IsDriven && EqualityComparer<T>.Default.Equals(field.Value, default!))
if (!field.IsDriven && EqualityComparer<TShared>.Default.Equals(field.Value, default!))
field.Value.Value = DefaultValue!;

field.Value.GetSyncWithVariable(VariableName);

var vuo = field.Value.OverrideForUser(field.World.LocalUser, _configKey.GetValue()!);
var vuo = field.Value.OverrideForUser(field.World.LocalUser, ConvertToShared(_configKey.GetValue())!);
vuo.CreateOverrideOnWrite.Value = true;

field.Value.OnValueChange += SharedValueChanged;
}

private void SharedValueChanged(SyncField<T> field)
private void SharedValueChanged(SyncField<TShared> field)
{
if (!AllowWriteBack || !_configKey.TrySetValue(field.Value, $"{SharedConfig.WriteBackPrefix}.{field.World.GetIdentifier()}"))
if (!AllowWriteBack || !_configKey.TrySetValue(ConvertToKey(field.Value)!, $"{SharedConfig.WriteBackPrefix}.{field.World.GetIdentifier()}"))
{
field.World.RunSynchronously(() => field.Value = _configKey.GetValue()!);
field.World.RunSynchronously(() => field.Value = ConvertToShared(_configKey.GetValue())!);
}
}

private void ValueChanged(object sender, ConfigKeyChangedEventArgs<T> configKeyChangedEventArgs)
private void ValueChanged(object sender, ConfigKeyChangedEventArgs<TKey> configKeyChangedEventArgs)
{
if (Engine.Current?.WorldManager is null)
return;

configKeyChangedEventArgs.TryGetWorldIdentifier(out var worldIdentifier);

foreach (var world in Engine.Current.WorldManager.Worlds.Where(world => world.GetIdentifier() != worldIdentifier))
world.RunSynchronously(() => GetSharedValue(world).Value.Value = configKeyChangedEventArgs.NewValue!);
world.RunSynchronously(() => GetSharedValue(world).Value.Value = ConvertToShared(configKeyChangedEventArgs.NewValue)!);
}
}

/// <summary>
/// Represents a wrapper for an <see cref="IDefiningConfigKey{T}"/>,
/// which makes its local value available as a shared resource in Resonite <see cref="World"/>s.<br/>
/// Optionally allows writing back changes from the <see cref="World"/> to the config item.
/// </summary>
/// <typeparam name="T">The type of the config item's value.</typeparam>
public sealed class ConfigKeySessionShare<T> : ConfigKeySessionShare<T, T>, IConfigKeySessionShare<T>
{
/// <summary>
/// Creates a new <see cref="IConfigKeySessionShare{T}"/> component,
/// which makes its config key's local value available as a shared resource in Resonite sessions.<br/>
/// Optionally allows writing back changes from the session to the config item.
/// </summary>
/// <param name="defaultValue">The default value for the shared config item for users that don't have it themselves.</param>
/// <param name="allowWriteBack">Whether to allow writing back changes from the session to the config item.</param>
public ConfigKeySessionShare(T? defaultValue = default!, bool allowWriteBack = false)
: base(Identity, Identity, defaultValue, allowWriteBack)
{ }

[return: NotNullIfNotNull(nameof(item))]
private static T? Identity(T? item) => item;
}

/// <summary>
/// Defines the untyped interface for config key components,
/// which make the key's local value available as a shared resource in Resonite sessions,
Expand Down Expand Up @@ -196,6 +260,26 @@ public interface IConfigKeySessionShare
/// </summary>
public string VariableName { get; }

/// <summary>
/// Converts the given value from the shared resource's type to the config item's.
/// </summary>
/// <remarks>
/// May throw when the provided input isn't compatible.
/// </remarks>
/// <param name="sharedValue">The value suitable for the shared resource to be converted.</param>
/// <returns>The value converted to the config item's type.</returns>
public object? ConvertToKey(object? sharedValue);

/// <summary>
/// Converts the given value from the config item's type to the shared resource's.
/// </summary>
/// <remarks>
/// May throw when the provided input isn't compatible.
/// </remarks>
/// <param name="keyValue">The value suitable for the config item to be converted.</param>
/// <returns>The value converted to the shared resource's type.</returns>
public object? ConvertToShared(object? keyValue);

/// <summary>
/// Gets a sequence of users who have defined overrides for the shared value.
/// </summary>
Expand All @@ -220,15 +304,45 @@ public interface IConfigKeySessionShare

/// <summary>
/// Defines the interface for config key components,
/// which make the key's local value available as a shared resource in Resonite sessions,
/// and optionally allow writing back changes from the session to the config item.
/// which make the key's local value available as a shared resource in Resonite <see cref="World"/>s,
/// and optionally allow writing back changes from the <see cref="World"/> to the config item.
/// </summary>
/// <typeparam name="T">The type of the config item's value.</typeparam>
public interface IConfigKeySessionShare<T> : IConfigKeyComponent<IDefiningConfigKey<T>>, IConfigKeySessionShare
/// <typeparam name="TKey">The type of the config item's value.</typeparam>
/// <typeparam name="TShared">The type of the resource shared in Resonite <see cref="World"/>s.</typeparam>
public interface IConfigKeySessionShare<TKey, TShared> : IConfigKeyComponent<IDefiningConfigKey<TKey>>, IConfigKeySessionShare
{
/// <summary>
/// Gets or sets the default value for the shared config item for users that don't have it themselves.
/// </summary>
public new T? DefaultValue { get; set; }
public new TShared? DefaultValue { get; set; }

/// <summary>
/// Converts the given value from the shared resource's type to the config item's.
/// </summary>
/// <remarks>
/// May throw when the provided input isn't compatible.
/// </remarks>
/// <param name="sharedValue">The value suitable for the shared resource to be converted.</param>
/// <returns>The value converted to the config item's type.</returns>
public TKey? ConvertToKey(TShared? sharedValue);

/// <summary>
/// Converts the given value from the config item's type to the shared resource's.
/// </summary>
/// <remarks>
/// May throw when the provided input isn't compatible.
/// </remarks>
/// <param name="keyValue">The value suitable for the config item to be converted.</param>
/// <returns>The value converted to the shared resource's type.</returns>
public TShared? ConvertToShared(TKey? keyValue);
}

/// <summary>
/// Defines the interface for config key components,
/// which make the key's local value available as a shared resource in Resonite sessions,
/// and optionally allow writing back changes from the session to the config item.
/// </summary>
/// <typeparam name="T">The type of the config item's value and the resource shared in Resonite <see cref="World"/>.</typeparam>
public interface IConfigKeySessionShare<T> : IConfigKeySessionShare<T, T>
{ }
}
29 changes: 28 additions & 1 deletion MonkeyLoader.Resonite.Integration/Configuration/SharedConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static class SharedConfig
/// </remarks>
public const string WriteBackPrefix = "SharedConfig.WriteBack";

private static readonly HashSet<IConfigKeySessionShare> _sharedConfigKeys = new();
private static readonly HashSet<IConfigKeySessionShare> _sharedConfigKeys = [];

/// <summary>
/// Gets the <see cref="World.SessionId"/> or the <see cref="World.Name"/>
Expand Down Expand Up @@ -61,6 +61,33 @@ public static Slot GetSharedConfigSlot(this World world)
public static Slot GetSharedConfigSlot(this World world, IConfigOwner configOwner)
=> world.GetSharedConfigSlot().FindChildOrAdd(configOwner.Id);

/// <summary>
/// Wraps the given <see cref="IDefiningConfigKey{T}"/> in a <see cref="ConfigKeySessionShare{T}"/>,
/// to make its local value available as a shared resource in Resonite sessions,
/// and optionally allow writing back changes from the session to the config item.
/// </summary>
/// <typeparam name="TKey">The type of the config item's value.</typeparam>
/// <typeparam name="TShared">
/// The type of the resource shared in Resonite <see cref="World"/>s.
/// Must be a valid generic parameter for <see cref="ValueField{T}"/> components.
/// </typeparam>
/// <param name="convertToShared">Converts the config item's value to the shared resource's.</param>
/// <param name="convertToKey">Converts the shared resource's value to the config item's.</param>
/// <param name="definingKey">The defining key to wrap.</param>
/// <param name="defaultValue">The default value for the shared config item for users that don't have it themselves.</param>
/// <param name="allowWriteBack">Whether to allow writing back changes from the session to the config item.</param>
/// <returns>The wrapped defining key.</returns>
public static IDefiningConfigKey<TKey> MakeShared<TKey, TShared>(this IEntity<IDefiningConfigKey<TKey>> definingKey,
Converter<TKey?, TShared?> convertToShared, Converter<TShared?, TKey?> convertToKey, TKey? defaultValue = default, bool allowWriteBack = false)
{
if (definingKey.Components.TryGet<IConfigKeySessionShare<TKey, TShared>>(out _))
return definingKey.Self;

definingKey.Add(new ConfigKeySessionShare<TKey, TShared>(convertToShared, convertToKey, defaultValue, allowWriteBack));

return definingKey.Self;
}

/// <summary>
/// Wraps the given <see cref="IDefiningConfigKey{T}"/> in a <see cref="ConfigKeySessionShare{T}"/>,
/// to make its local value available as a shared resource in Resonite sessions,
Expand Down
Loading
Loading