Files
MRCC/launcher/PCL-CE/PCL.Core/App/Configuration/ConfigItem.cs
T
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

370 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using PCL.Core.Utils.Exts;
namespace PCL.Core.App.Configuration;
/// <summary>
/// 配置项。
/// </summary>
/// <typeparam name="TValue">值类型</typeparam>
public class ConfigItem<TValue>(
string key,
Func<TValue> defaultValue,
ConfigSource source
) : IConfigScope, ConfigItem
{
public string Key { get; } = key;
public ConfigSource Source { get; } = source;
public Type Type => typeof(TValue);
private Func<TValue>? _defaultValueConstructor = defaultValue;
private TValue? _defaultValue;
private bool _defaultValueHasSet = false;
#region 默认值逻辑
private TValue _GetDefaultValue()
{
if (_defaultValueHasSet) return _defaultValue!;
_defaultValue = _defaultValueConstructor!();
_defaultValueHasSet = true;
_defaultValueConstructor = null;
return _defaultValue;
}
/// <summary>
/// 默认值。
/// </summary>
public TValue DefaultValue => _GetDefaultValue();
public object DefaultValueNoType => DefaultValue ?? default!;
#endregion
public ConfigItem(string key, TValue defaultValue, ConfigSource source)
: this(key, () => defaultValue, source) { }
public IEnumerable<string> CheckScope(IReadOnlySet<string> keys) => keys.Contains(Key) ? [Key] : [];
#region 值获取和修改
private IConfigProvider _Provider { get => field ??= ConfigService.GetProvider(Source); } = null!;
private ConfigValueCache<TValue> _valueCache = new();
/// <summary>
/// 指定是否启用缓存。<br/>
/// <b>NOTE</b>: 禁用缓存将造成一些功能(如自动监听内容更改)不按预期工作,请仅在真正需要的时候禁用。
/// </summary>
public bool EnableCache
{
get;
set
{
if (!field) _valueCache.InvalidateAll();
field = value;
}
} = true;
/// <summary>
/// 处理看起来是新的值,并返回是否真的是新的。<br/>
/// 只有启用缓存时该方法才会生效,未启用缓存将始终直接返回 <see langword="true"/>。
/// </summary>
private bool _ProcessNewCache(TValue newCache, object? argument, bool force = false)
{
if (!EnableCache) return true;
if (!force)
{
// 判断是否是新值
var existsOld = _valueCache.TryRead(out var oldCache, argument);
if (existsOld && EqualityComparer<TValue>.Default.Equals(oldCache, newCache)) return false;
}
// 对新缓存值执行准备工作
if (newCache is INotifyPropertyChanged reactive)
reactive.PropertyChanged += (_, _) => OnContentChanged();
else if (newCache is INotifyCollectionChanged reactiveCollection)
reactiveCollection.CollectionChanged += (_, _) => OnContentChanged();
// 写入缓存
_valueCache.Write(newCache, argument);
return true;
void OnContentChanged() => SetValue(newCache, argument, bypassCache: true);
}
/// <summary>
/// 获取配置值。
/// </summary>
/// <param name="argument">上下文参数</param>
/// <returns>已设置的配置值或默认值</returns>
public TValue GetValue(object? argument = null)
{
TValue? value = default; // 这个初始化是多余的,但是煞笔巨硬不初始化会报错
var exists = EnableCache && _valueCache.TryRead(out value, argument);
var newValue = false;
if (!exists)
{
newValue = true;
exists = _Provider.GetValue(Key, out value, argument);
}
var e = _TriggerEvent(ConfigEvent.Get, argument, value, true);
if (e is not null)
{
if (e.Cancelled) return DefaultValue;
if (e.NewValueReplacement is not null) return (TValue)e.NewValueReplacement;
}
if (!exists) value = DefaultValue;
if (newValue) _ProcessNewCache(value!, argument);
return value!;
}
public object GetValueNoType(object? argument = null)
{
return GetValue(argument) ?? default!;
}
/// <summary>
/// 设置配置值。
/// </summary>
/// <param name="value">用于设置的值</param>
/// <param name="argument">上下文参数</param>
/// <param name="forceNewValue">强制将传入的值视为新值,不检查缓存,仅在 <see cref="EnableCache"/> 为 <see langword="true"/> 时生效</param>
/// <param name="bypassCache">跳过缓存检查和写入,相当于对本次操作临时将 <see cref="EnableCache"/> 设为 <see langword="false"/></param>
/// <returns>是否成功设置值,若成功则为 <c>true</c></returns>
public bool SetValue(TValue value, object? argument = null, bool forceNewValue = false, bool bypassCache = false)
{
var e = _TriggerEvent(ConfigEvent.Set, argument, value, isPreview: true);
if (e is not null)
{
if (e.Cancelled) return false;
if (e.NewValueReplacement is not null) value = (TValue)e.NewValueReplacement;
}
if (bypassCache || _ProcessNewCache(value, argument, forceNewValue))
_Provider.SetValue(Key, value, argument);
_TriggerEvent(ConfigEvent.Set, argument, value, e: e, isPreview: false);
return true;
}
public bool SetValueNoType(object value, object? argument = null)
{
try
{
return SetValue((TValue)value, argument);
}
catch (InvalidCastException)
{
// 兼容龙猫妙妙小代码直接传入 string 值的行为
if (value is string v) return SetValue(v.Convert<TValue>()!, argument);
var msg = $"Value convert failed (required: {Type.FullName}, provided: {value.GetType().FullName})";
throw new InvalidCastException(msg);
}
}
public bool SetDefaultValue(object? argument = null, bool? forceNewValue = null)
{
return SetValue(DefaultValue, argument, forceNewValue ?? IsDefault(argument));
}
public bool Reset(object? argument = null)
{
var e = _TriggerEvent(ConfigEvent.Reset, argument, null, isPreview: true);
if (e is { Cancelled: true }) return false;
_Provider.Delete(Key, argument);
if (EnableCache) _valueCache.Invalidate(argument);
_TriggerEvent(ConfigEvent.Reset, argument, DefaultValueNoType, isPreview: false);
return true;
}
public bool IsDefault(object? argument = null)
{
var result = !_Provider.Exists(Key, argument);
var e = _TriggerEvent(ConfigEvent.CheckDefault, argument, result);
if (e is { NewValueReplacement: not null }) result = (bool)e.NewValueReplacement;
return result;
}
#endregion
#region 事件处理
private readonly HashSet<ConfigObserver> _observers = [];
private readonly HashSet<ConfigObserver> _previewObservers = [];
public void Observe(ConfigObserver observer)
{
if (observer.IsPreview) _previewObservers.Add(observer);
else _observers.Add(observer);
}
public bool Unobserve(ConfigObserver observer)
=> observer.IsPreview ? _previewObservers.Remove(observer) : _observers.Remove(observer);
// 获取值,若未设置则返回 null
private object? _GetValueOrNull(object? argument)
{
var exists = _Provider.GetValue<TValue>(Key, out var value, argument);
return exists ? value : null;
}
public ConfigEventArgs? TriggerEvent(
ConfigEvent trigger, object? argument,
bool bypassOldValue = false, bool fillNewValue = false)
{
return _TriggerEvent(trigger, argument, null, bypassOldValue, fillNewValue);
}
private ConfigEventArgs? _TriggerEvent(
ConfigEvent trigger, object? argument, object? newValue,
bool bypassOldValue = false, bool fillNewValue = false,
ConfigEventArgs? e = null, bool? isPreview = null)
{
var replaceNewValue = false;
foreach (var observer in (
from observer in (isPreview is { } p ? (p ? _previewObservers : _observers) : _previewObservers.Concat(_observers))
let logic = (int)observer.Event & (int)trigger
where logic > 0
select observer
)) {
if (e is null)
{
if (isPreview == false && !bypassOldValue) bypassOldValue = true;
var currentValue = (fillNewValue || !bypassOldValue) ? _GetValueOrNull(argument) : null;
if (newValue is null && fillNewValue) newValue = currentValue ?? DefaultValue;
e = new ConfigEventArgs(this, trigger, argument, bypassOldValue ? null : currentValue, newValue);
}
observer.Handler(e);
// 对 preview 的特殊处理
if (observer.IsPreview)
{
if (e.NewValueReplacement is not null) replaceNewValue = true; // 记录替换操作
if (e.Cancelled) return e;
}
// 防止非 preview 事件传递替换值
else if (!replaceNewValue && e.NewValueReplacement is not null) e.NewValueReplacement = null;
}
// 防止非 preview 事件传递取消状态
if (e is { Cancelled: true }) e.Cancelled = false;
return e;
}
#endregion
}
/// <summary>
/// <see cref="ConfigItem{TValue}"/> 的非泛型方法抽象层,用于手动解决巨硬
/// 2025 年仍未支持的极其先进的隐式去泛型化。
/// </summary>
// ReSharper disable once InconsistentNaming
public interface ConfigItem
{
/// <summary>
/// 配置键。
/// </summary>
public string Key { get; }
/// <summary>
/// 配置来源。
/// </summary>
public ConfigSource Source { get; }
/// <summary>
/// 配置的 CLR 类型。
/// </summary>
public Type Type { get; }
/// <summary>
/// 传入事件观察器以观察事件。
/// </summary>
public void Observe(ConfigObserver observer);
/// <summary>
/// 取消观察事件。
/// </summary>
public bool Unobserve(ConfigObserver observer);
/// <summary>
/// 触发配置项事件。
/// </summary>
/// <param name="trigger">触发事件</param>
/// <param name="argument">上下文参数</param>
/// <param name="bypassOldValue">若为 <c>true</c> 则向事件参数的旧值传递 <c>null</c>,否则传递当前值</param>
/// <param name="fillNewValue">若为 <c>true</c>,当新值为 <c>null</c> 时将传递当前值或默认值</param>
/// <returns></returns>
public ConfigEventArgs? TriggerEvent(
ConfigEvent trigger,
object? argument,
bool bypassOldValue = false,
bool fillNewValue = false
);
/// <summary>
/// 传入事件类型与处理委托以观察事件。
/// </summary>
public ConfigObserver Observe(ConfigEvent trigger, ConfigEventHandler handler, bool isPreview = false)
{
var observer = new ConfigObserver(trigger, handler, isPreview);
Observe(observer);
return observer;
}
/// <summary>
/// 传统的用于兼容的值改变事件。<br/>
/// 请尽可能避免使用,而是使用 <see cref="RegisterConfigEventAttribute"/>
/// 来声明事件观察,或使用 <see cref="Observe(ConfigObserver)"/> 和
/// <see cref="Unobserve(ConfigObserver)"/> 来灵活管理事件。
/// </summary>
public event ConfigEventHandler Changed
{
add => Observe(ConfigEvent.Changed, value);
remove => throw new NotSupportedException("Please use Observe() and Unobserve() to access advanced event management");
}
/// <summary>
/// 重置配置值,使其变为未设置状态。
/// </summary>
/// <param name="argument">上下文参数</param>
/// <returns>是否成功重置值,若成功则为 <c>true</c></returns>
public bool Reset(object? argument = null);
/// <summary>
/// 检查配置值是否为默认值 (未设置状态)
/// </summary>
/// <param name="argument">上下文参数</param>
public bool IsDefault(object? argument = null);
/// <summary>
/// 将配置项的值设置为默认值,设置后 <see cref="IsDefault"/> 将返回 <c>false</c>。
/// </summary>
/// <param name="argument">上下文参数</param>
/// <param name="forceNewValue">强制视为新值,不检查缓存,仅在 <see cref="EnableCache"/> 为 <see langword="true"/> 时生效</param>
/// <returns>是否成功设置值,若成功则为 <c>true</c></returns>
public bool SetDefaultValue(object? argument = null, bool? forceNewValue = null);
/// <summary>
/// 没有泛型的 <see cref="ConfigItem{T}.GetValue"/>。<br/>
/// 我们都不想给非引用类型装箱,但是龙猫想。
/// </summary>
public object GetValueNoType(object? argument = null);
/// <summary>
/// 没有泛型的 <see cref="ConfigItem{T}.SetValue"/>。<br/>
/// 我们都不想给非引用类型装箱,但是龙猫想。
/// </summary>
public bool SetValueNoType(object value, object? argument = null);
/// <summary>
/// 没有泛型的 <see cref="ConfigItem{T}.DefaultValue"/>。<br/>
/// 我们都不想给非引用类型装箱,但是龙猫想。
/// </summary>
public object DefaultValueNoType { get; }
/// <summary>
/// 是否启用值缓存,默认为 <c>true</c>。设为 <c>false</c> 将清除已存在的缓存。
/// </summary>
public bool EnableCache { get; set; }
}