CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
370 lines
14 KiB
C#
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; }
|
|
}
|