mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-02-15 17:27:26 +08:00
[RF] AquaMai configuration refactor (#82)
更新了配置文件格式,原有的配置文件将被自动无缝迁移,详情请见新的配置文件中的注释(例外:`SlideJudgeTweak` 不再默认启用) 旧配置文件将被重命名备份,如果更新到此版本遇到 Bug 请联系我们 Updated configuration file schema. The old config file will be migrated automatically and seamlessly. See the comments in the new configuration file for details. (Except for `SlideJudgeTweak` is no longer enabled by default) Your old configuration file will be renamed as a backup. If you encounter any bug with this version, please contact us.
This commit is contained in:
60
AquaMai/AquaMai.Config/AquaMai.Config.csproj
Normal file
60
AquaMai/AquaMai.Config/AquaMai.Config.csproj
Normal file
@@ -0,0 +1,60 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{DF1536F9-3B06-4463-B654-4CC3E708B610}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>AquaMai.Config</RootNamespace>
|
||||
<AssemblyName>AquaMai.Config</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>12</LangVersion>
|
||||
<NoWarn>414</NoWarn>
|
||||
<AssemblySearchPaths>$(ProjectDir)../Libs/;$(AssemblySearchPaths)</AssemblySearchPaths>
|
||||
<OutputPath>$(ProjectDir)../Output/</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<DebugType>None</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DefineConstants>DEBUG</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../AquaMai.Config.Interfaces/AquaMai.Config.Interfaces.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="mscorlib" />
|
||||
<Reference Include="Mono.Cecil" />
|
||||
<Reference Include="System" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="FodyWeavers.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fody" Version="6.8.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ILMerge.Fody" Version="1.24.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Samboy063.Tomlet" Version="5.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
// When The most inner namespace is the same name of the class, it should be collapsed.
|
||||
// The class must be the only class in the namespace with a [ConfigSection] attribute.
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ConfigCollapseNamespaceAttribute : Attribute
|
||||
{}
|
||||
13
AquaMai/AquaMai.Config/Attributes/ConfigComment.cs
Normal file
13
AquaMai/AquaMai.Config/Attributes/ConfigComment.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
public record ConfigComment(string CommentEn, string CommentZh)
|
||||
{
|
||||
public string GetLocalized(string lang) => lang switch
|
||||
{
|
||||
"en" => CommentEn ?? "",
|
||||
"zh" => CommentZh ?? "",
|
||||
_ => throw new ArgumentException($"Unsupported language: {lang}")
|
||||
};
|
||||
}
|
||||
24
AquaMai/AquaMai.Config/Attributes/ConfigEntryAttribute.cs
Normal file
24
AquaMai/AquaMai.Config/Attributes/ConfigEntryAttribute.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
public enum SpecialConfigEntry
|
||||
{
|
||||
None,
|
||||
Locale
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class ConfigEntryAttribute(
|
||||
string en = null,
|
||||
string zh = null,
|
||||
// NOTE: Don't use this argument to hide any useful options.
|
||||
// Only use it to hide options that really won't be used.
|
||||
bool hideWhenDefault = false,
|
||||
// NOTE: Use this argument to mark special config entries that need special handling.
|
||||
SpecialConfigEntry specialConfigEntry = SpecialConfigEntry.None) : Attribute
|
||||
{
|
||||
public ConfigComment Comment { get; } = new ConfigComment(en, zh);
|
||||
public bool HideWhenDefault { get; } = hideWhenDefault;
|
||||
public SpecialConfigEntry SpecialConfigEntry { get; } = specialConfigEntry;
|
||||
}
|
||||
21
AquaMai/AquaMai.Config/Attributes/ConfigSectionAttribute.cs
Normal file
21
AquaMai/AquaMai.Config/Attributes/ConfigSectionAttribute.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ConfigSectionAttribute(
|
||||
string en = null,
|
||||
string zh = null,
|
||||
// It will be hidden if the default value is preserved.
|
||||
bool exampleHidden = false,
|
||||
// A "Disabled = true" entry is required to disable the section.
|
||||
bool defaultOn = false,
|
||||
// NOTE: You probably shouldn't use this. Only the "General" section is using this.
|
||||
// Implies defaultOn = true.
|
||||
bool alwaysEnabled = false) : Attribute
|
||||
{
|
||||
public ConfigComment Comment { get; } = new ConfigComment(en, zh);
|
||||
public bool ExampleHidden { get; } = exampleHidden;
|
||||
public bool DefaultOn { get; } = defaultOn || alwaysEnabled;
|
||||
public bool AlwaysEnabled { get; } = alwaysEnabled;
|
||||
}
|
||||
78
AquaMai/AquaMai.Config/Attributes/EnableCondition.cs
Normal file
78
AquaMai/AquaMai.Config/Attributes/EnableCondition.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
|
||||
namespace AquaMai.Config.Attributes;
|
||||
|
||||
public enum EnableConditionOperator
|
||||
{
|
||||
Equal,
|
||||
NotEqual,
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
GreaterThanOrEqual,
|
||||
LessThanOrEqual
|
||||
}
|
||||
|
||||
public class EnableCondition(
|
||||
Type referenceType,
|
||||
string referenceMember,
|
||||
EnableConditionOperator @operator,
|
||||
object rightSideValue) : Attribute
|
||||
{
|
||||
public Type ReferenceType { get; } = referenceType;
|
||||
public string ReferenceMember { get; } = referenceMember;
|
||||
public EnableConditionOperator Operator { get; } = @operator;
|
||||
public object RightSideValue { get; } = rightSideValue;
|
||||
|
||||
// Referencing a field in another class and checking if it's true.
|
||||
public EnableCondition(Type referenceType, string referenceMember)
|
||||
: this(referenceType, referenceMember, EnableConditionOperator.Equal, true)
|
||||
{ }
|
||||
|
||||
// Referencing a field in the same class and comparing it with a value.
|
||||
public EnableCondition(string referenceMember, EnableConditionOperator condition, object value)
|
||||
: this(null, referenceMember, condition, value)
|
||||
{ }
|
||||
|
||||
// Referencing a field in the same class and checking if it's true.
|
||||
public EnableCondition(string referenceMember)
|
||||
: this(referenceMember, EnableConditionOperator.Equal, true)
|
||||
{ }
|
||||
|
||||
public bool Evaluate(Type selfType)
|
||||
{
|
||||
var referenceType = ReferenceType ?? selfType;
|
||||
var referenceField = referenceType.GetField(
|
||||
ReferenceMember,
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
|
||||
var referenceProperty = referenceType.GetProperty(
|
||||
ReferenceMember,
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
|
||||
if (referenceField == null && referenceProperty == null)
|
||||
{
|
||||
throw new ArgumentException($"Field or property {ReferenceMember} not found in {referenceType.FullName}");
|
||||
}
|
||||
var referenceMemberValue = referenceField != null ? referenceField.GetValue(null) : referenceProperty.GetValue(null);
|
||||
switch (Operator)
|
||||
{
|
||||
case EnableConditionOperator.Equal:
|
||||
return referenceMemberValue.Equals(RightSideValue);
|
||||
case EnableConditionOperator.NotEqual:
|
||||
return !referenceMemberValue.Equals(RightSideValue);
|
||||
case EnableConditionOperator.GreaterThan:
|
||||
case EnableConditionOperator.LessThan:
|
||||
case EnableConditionOperator.GreaterThanOrEqual:
|
||||
case EnableConditionOperator.LessThanOrEqual:
|
||||
var comparison = (IComparable)referenceMemberValue;
|
||||
return Operator switch
|
||||
{
|
||||
EnableConditionOperator.GreaterThan => comparison.CompareTo(RightSideValue) > 0,
|
||||
EnableConditionOperator.LessThan => comparison.CompareTo(RightSideValue) < 0,
|
||||
EnableConditionOperator.GreaterThanOrEqual => comparison.CompareTo(RightSideValue) >= 0,
|
||||
EnableConditionOperator.LessThanOrEqual => comparison.CompareTo(RightSideValue) <= 0,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
98
AquaMai/AquaMai.Config/Config.cs
Normal file
98
AquaMai/AquaMai.Config/Config.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using AquaMai.Config.Reflection;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public class Config : IConfig
|
||||
{
|
||||
// NOTE: If a section's state is default, all underlying entries' states are default as well.
|
||||
|
||||
public record SectionState : IConfig.ISectionState
|
||||
{
|
||||
public bool IsDefault { get; set; }
|
||||
public bool DefaultEnabled { get; init; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
public record EntryState : IConfig.IEntryState
|
||||
{
|
||||
public bool IsDefault { get; set; }
|
||||
public object DefaultValue { get; init; }
|
||||
public object Value { get; set; }
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, SectionState> sections = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, EntryState> entries = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public readonly ReflectionManager reflectionManager;
|
||||
public IReflectionManager ReflectionManager => reflectionManager;
|
||||
|
||||
public Config(ReflectionManager reflectionManager)
|
||||
{
|
||||
this.reflectionManager = reflectionManager;
|
||||
|
||||
foreach (var section in reflectionManager.SectionValues)
|
||||
{
|
||||
InitializeSection(section);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeSection(ReflectionManager.Section section)
|
||||
{
|
||||
sections.Add(section.Path, new SectionState()
|
||||
{
|
||||
IsDefault = true,
|
||||
DefaultEnabled = section.Attribute.DefaultOn,
|
||||
Enabled = section.Attribute.DefaultOn
|
||||
});
|
||||
|
||||
foreach (var entry in section.Entries)
|
||||
{
|
||||
var defaultValue = entry.Field.GetValue(null);
|
||||
if (defaultValue == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Null default value for entry {entry.Path} is not allowed.");
|
||||
}
|
||||
entries.Add(entry.Path, new EntryState()
|
||||
{
|
||||
IsDefault = true,
|
||||
DefaultValue = defaultValue,
|
||||
Value = defaultValue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public IConfig.ISectionState GetSectionState(IReflectionManager.ISection section)
|
||||
{
|
||||
return sections[section.Path];
|
||||
}
|
||||
|
||||
public IConfig.ISectionState GetSectionState(Type type)
|
||||
{
|
||||
if (!ReflectionManager.TryGetSection(type, out var section))
|
||||
{
|
||||
throw new ArgumentException($"Type {type.FullName} is not a config section.");
|
||||
}
|
||||
return sections[section.Path];
|
||||
}
|
||||
|
||||
public void SetSectionEnabled(IReflectionManager.ISection section, bool enabled)
|
||||
{
|
||||
sections[section.Path].IsDefault = false;
|
||||
sections[section.Path].Enabled = enabled;
|
||||
}
|
||||
|
||||
public IConfig.IEntryState GetEntryState(IReflectionManager.IEntry entry)
|
||||
{
|
||||
return entries[entry.Path];
|
||||
}
|
||||
|
||||
public void SetEntryValue(IReflectionManager.IEntry entry, object value)
|
||||
{
|
||||
entry.Field.SetValue(null, value);
|
||||
entries[entry.Path].IsDefault = false;
|
||||
entries[entry.Path].Value = value;
|
||||
}
|
||||
}
|
||||
125
AquaMai/AquaMai.Config/ConfigParser.cs
Normal file
125
AquaMai/AquaMai.Config/ConfigParser.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using Tomlet.Models;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using AquaMai.Config.Reflection;
|
||||
using AquaMai.Config.Migration;
|
||||
using System.Linq;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public class ConfigParser : IConfigParser
|
||||
{
|
||||
public readonly static ConfigParser Instance = new();
|
||||
|
||||
private readonly static string[] supressUnrecognizedConfigPaths = ["Version"];
|
||||
private readonly static string[] supressUnrecognizedConfigPathSuffixes = [
|
||||
".Disabled", // For section enable state.
|
||||
".Disable", // For section enable state, but the wrong key, warn later.
|
||||
".Enabled", // For section enable state, but the wrong key, warn later.
|
||||
".Enable", // For section enable state, but the wrong key, warn later.
|
||||
];
|
||||
|
||||
private ConfigParser()
|
||||
{}
|
||||
|
||||
public void Parse(IConfig config, string tomlString)
|
||||
{
|
||||
var configView = new ConfigView(tomlString);
|
||||
Parse(config, configView);
|
||||
}
|
||||
|
||||
public void Parse(IConfig config, IConfigView configView)
|
||||
{
|
||||
var configVersion = ConfigMigrationManager.Instance.GetVersion(configView);
|
||||
if (configVersion != ConfigMigrationManager.Instance.latestVersion)
|
||||
{
|
||||
throw new InvalidOperationException($"Config version mismatch: expected {ConfigMigrationManager.Instance.latestVersion}, got {configVersion}");
|
||||
}
|
||||
Hydrate((Config)config, ((ConfigView)configView).root, "");
|
||||
}
|
||||
|
||||
private static void Hydrate(Config config, TomlValue value, string path)
|
||||
{
|
||||
if (config.ReflectionManager.TryGetSection(path, out var section))
|
||||
{
|
||||
ParseSectionEnableState(config, (ReflectionManager.Section)section, value, path);
|
||||
}
|
||||
|
||||
if (value is TomlTable table)
|
||||
{
|
||||
bool isLeaf = true;
|
||||
foreach (var subKey in table.Keys)
|
||||
{
|
||||
var subValue = table.GetValue(subKey);
|
||||
var subPath = path == "" ? subKey : $"{path}.{subKey}";
|
||||
if (subValue is TomlTable)
|
||||
{
|
||||
isLeaf = false;
|
||||
}
|
||||
Hydrate(config, subValue, subPath);
|
||||
}
|
||||
// A leaf dictionary, which has no child dictionaries, must be a section.
|
||||
if (isLeaf && section == null)
|
||||
{
|
||||
Utility.Log($"Unrecognized config section: {path}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's an config entry value (or a primitive type for enabling a section).
|
||||
if (!config.ReflectionManager.ContainsSection(path) &&
|
||||
!config.ReflectionManager.ContainsEntry(path) &&
|
||||
!supressUnrecognizedConfigPaths.Any(s => path.Equals(s, StringComparison.OrdinalIgnoreCase)) &&
|
||||
!supressUnrecognizedConfigPathSuffixes.Any(suffix => path.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Utility.Log($"Unrecognized config entry: {path}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.ReflectionManager.TryGetEntry(path, out var entry))
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsedValue = Utility.ParseTomlValue(entry.Field.FieldType, value);
|
||||
config.SetEntryValue(entry, parsedValue);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Utility.Log($"Error hydrating config ({path} = {value.StringValue}): {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ParseSectionEnableState(
|
||||
Config config,
|
||||
ReflectionManager.Section section,
|
||||
TomlValue value,
|
||||
string path)
|
||||
{
|
||||
if (value is TomlTable table)
|
||||
{
|
||||
foreach (var unexpectedKey in (string[]) ["Enable", "Enabled", "Disable"])
|
||||
{
|
||||
if (Utility.TomlContainsKeyCaseInsensitive(table, unexpectedKey))
|
||||
{
|
||||
Utility.Log($"Unexpected key \"{unexpectedKey}\" for enable status under \"{path}\". Only \"Disabled\" is parsed.");
|
||||
}
|
||||
}
|
||||
|
||||
if (Utility.TomlTryGetValueCaseInsensitive(table, "Disabled", out var disableValue) && !section.Attribute.AlwaysEnabled)
|
||||
{
|
||||
var disabled = Utility.IsTruty(disableValue, path + ".Disabled");
|
||||
config.SetSectionEnabled(section, !disabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
config.SetSectionEnabled(section, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
config.SetSectionEnabled(section, Utility.IsTruty(value, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
185
AquaMai/AquaMai.Config/ConfigSerializer.cs
Normal file
185
AquaMai/AquaMai.Config/ConfigSerializer.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using Tomlet.Models;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public class ConfigSerializer(IConfigSerializer.Options Options) : IConfigSerializer
|
||||
{
|
||||
private const string BANNER_ZH =
|
||||
"""
|
||||
这是 AquaMai 的 TOML 配置文件
|
||||
|
||||
- 井号 # 开头的行为注释,被注释掉的内容不会生效
|
||||
- 为方便使用 VSCode 等编辑器进行编辑,被注释掉的配置内容使用一个井号 #,而注释文本使用两个井号 ##
|
||||
- 以方括号包裹的行,如 [OptionalCategory.Section],为一个栏目
|
||||
- 将默认被注释(即默认禁用)的栏目取消注释即可启用
|
||||
- 若要禁用一个默认启用的栏目,请在栏目下添加「Disabled = true」配置项,删除它/注释它不会有效
|
||||
- 形如「键 = 值」为一个配置项
|
||||
- 配置项应用到其上方最近的栏目,请不要在一个栏目被注释掉的情况下开启其配置项(会加到上一个栏目,无效)
|
||||
- 当对应栏目启用时,配置项生效,无论是否将其取消注释
|
||||
- 注释掉的配置项保留其注释中的默认值,默认值可能会随版本更新而变化
|
||||
- 该文件的格式和文字注释是固定的,配置文件将在启动时被重写,无法解析的内容将被删除
|
||||
|
||||
试试使用 MaiChartManager 图形化配置 AquaMai 吧!
|
||||
https://github.com/clansty/MaiChartManager
|
||||
""";
|
||||
|
||||
private const string BANNER_EN =
|
||||
"""
|
||||
This is the TOML configuration file of AquaMai.
|
||||
|
||||
- Lines starting with a hash # are comments. Commented content will not take effect.
|
||||
- For easily editing with editors (e.g. VSCode), commented configuration content uses a single hash #, while comment text uses two hashes ##.
|
||||
- Lines with square brackets like [OptionalCategory.Section] are sections.
|
||||
- Uncomment a section that is commented out by default (i.e. disabled by default) to enable it.
|
||||
- To disable a section that is enabled by default, add a "Disable = true" entry under the section. Removing/commenting it will not work.
|
||||
- Lines like "Key = Value" is a configuration entry.
|
||||
- Configuration entries apply to the nearest section above them. Do not enable a configuration entry when its section is commented out (will be added to the previous section, which is invalid).
|
||||
- Configuration entries take effect when the corresponding section is enabled, regardless of whether they are uncommented.
|
||||
- Commented configuration entries retain their default values (shown in the comment), which may change with version updates.
|
||||
- The format and text comments of this file are fixed. The configuration file will be rewritten at startup, and unrecognizable content will be deleted.
|
||||
""";
|
||||
|
||||
private readonly IConfigSerializer.Options Options = Options;
|
||||
|
||||
public string Serialize(IConfig config)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
if (Options.IncludeBanner)
|
||||
{
|
||||
var banner = Options.Lang == "zh" ? BANNER_ZH : BANNER_EN;
|
||||
if (banner != null)
|
||||
{
|
||||
AppendComment(sb, banner.TrimEnd());
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
// Version
|
||||
AppendEntry(sb, null, "Version", "2.0");
|
||||
|
||||
foreach (var section in ((Config)config).reflectionManager.SectionValues)
|
||||
{
|
||||
var sectionState = config.GetSectionState(section);
|
||||
|
||||
// If the state is default, print the example only. If the example is hidden, skip it.
|
||||
if (sectionState.IsDefault && section.Attribute.ExampleHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
AppendComment(sb, section.Attribute.Comment);
|
||||
|
||||
if (// If the section is hidden and hidden by default, print it as commented.
|
||||
sectionState.IsDefault && !sectionState.Enabled &&
|
||||
// If the section is marked as always enabled, print it normally.
|
||||
!section.Attribute.AlwaysEnabled)
|
||||
{
|
||||
sb.AppendLine($"#[{section.Path}]");
|
||||
}
|
||||
else // If the section is overridden, or is enabled by any means, print it normally.
|
||||
{
|
||||
sb.AppendLine($"[{section.Path}]");
|
||||
}
|
||||
|
||||
if (!section.Attribute.AlwaysEnabled)
|
||||
{
|
||||
// If the section is disabled explicitly, print the "Disabled" meta entry.
|
||||
if (!sectionState.IsDefault && !sectionState.Enabled)
|
||||
{
|
||||
AppendEntry(sb, null, "Disabled", value: true);
|
||||
}
|
||||
// If the section is enabled by default, print the "Disabled" meta entry as commented.
|
||||
else if (sectionState.IsDefault && section.Attribute.DefaultOn)
|
||||
{
|
||||
AppendEntry(sb, null, "Disabled", value: false, commented: true);
|
||||
}
|
||||
// Otherwise, don't print the "Disabled" meta entry.
|
||||
}
|
||||
|
||||
// Print entries.
|
||||
foreach (var entry in section.entries)
|
||||
{
|
||||
var entryState = config.GetEntryState(entry);
|
||||
AppendComment(sb, entry.Attribute.Comment);
|
||||
if (entry.Attribute.SpecialConfigEntry == SpecialConfigEntry.Locale && Options.OverrideLocaleValue)
|
||||
{
|
||||
AppendEntry(sb, section.Path, entry.Name, Options.Lang);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendEntry(sb, section.Path, entry.Name, entryState.Value, entryState.IsDefault);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string SerializeTomlValue(string diagnosticPath, object value)
|
||||
{
|
||||
var type = value.GetType();
|
||||
if (value is bool b)
|
||||
{
|
||||
return b ? "true" : "false";
|
||||
}
|
||||
else if (value is string str)
|
||||
{
|
||||
return new TomlString(str).SerializedValue;
|
||||
}
|
||||
else if (type.IsEnum)
|
||||
{
|
||||
return new TomlString(value.ToString()).SerializedValue;
|
||||
}
|
||||
else if (Utility.IsIntegerType(type))
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
else if (Utility.IsFloatType(type))
|
||||
{
|
||||
return new TomlDouble(Convert.ToDouble(value)).SerializedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentMethod = MethodBase.GetCurrentMethod();
|
||||
throw new NotImplementedException($"Unsupported config entry type: {type.FullName} ({diagnosticPath}). Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendComment(StringBuilder sb, ConfigComment comment)
|
||||
{
|
||||
if (comment != null)
|
||||
{
|
||||
AppendComment(sb, comment.GetLocalized(Options.Lang));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendComment(StringBuilder sb, string comment)
|
||||
{
|
||||
comment = comment.Trim();
|
||||
if (!string.IsNullOrEmpty(comment))
|
||||
{
|
||||
foreach (var line in comment.Split('\n'))
|
||||
{
|
||||
sb.AppendLine($"## {line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendEntry(StringBuilder sb, string diagnosticsSection, string key, object value, bool commented = false)
|
||||
{
|
||||
if (commented)
|
||||
{
|
||||
sb.Append('#');
|
||||
}
|
||||
var diagnosticsPath = string.IsNullOrEmpty(diagnosticsSection)
|
||||
? key
|
||||
: $"{diagnosticsSection}.{key}";
|
||||
sb.AppendLine($"{key} = {SerializeTomlValue(diagnosticsPath, value)}");
|
||||
}
|
||||
}
|
||||
105
AquaMai/AquaMai.Config/ConfigView.cs
Normal file
105
AquaMai/AquaMai.Config/ConfigView.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using Tomlet;
|
||||
using Tomlet.Models;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public class ConfigView : IConfigView
|
||||
{
|
||||
public readonly TomlTable root;
|
||||
|
||||
public ConfigView()
|
||||
{
|
||||
root = new TomlTable();
|
||||
}
|
||||
|
||||
public ConfigView(TomlTable root)
|
||||
{
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public ConfigView(string tomlString)
|
||||
{
|
||||
var tomlValue = new TomlParser().Parse(tomlString);
|
||||
if (tomlValue is not TomlTable tomlTable)
|
||||
{
|
||||
throw new ArgumentException($"Invalid TOML, expected a table, got: {tomlValue.GetType()}");
|
||||
}
|
||||
root = tomlTable;
|
||||
}
|
||||
|
||||
public TomlTable EnsureDictionary(string path)
|
||||
{
|
||||
var pathComponents = path.Split('.');
|
||||
var current = root;
|
||||
foreach (var component in pathComponents)
|
||||
{
|
||||
if (!current.TryGetValue(component, out var next))
|
||||
{
|
||||
next = new TomlTable();
|
||||
current.Put(component, next);
|
||||
}
|
||||
current = (TomlTable)next;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
public void SetValue(string path, object value)
|
||||
{
|
||||
var pathComponents = path.Split('.');
|
||||
var current = root;
|
||||
foreach (var component in pathComponents.Take(pathComponents.Length - 1))
|
||||
{
|
||||
if (!current.TryGetValue(component, out var next))
|
||||
{
|
||||
next = new TomlTable();
|
||||
current.Put(component, next);
|
||||
}
|
||||
current = (TomlTable)next;
|
||||
}
|
||||
current.Put(pathComponents.Last(), value);
|
||||
}
|
||||
|
||||
public T GetValueOrDefault<T>(string path, T defaultValue = default)
|
||||
{
|
||||
return TryGetValue(path, out T resultValue) ? resultValue : defaultValue;
|
||||
}
|
||||
|
||||
public bool TryGetValue<T>(string path, out T resultValue)
|
||||
{
|
||||
var pathComponents = path.Split('.');
|
||||
var current = root;
|
||||
foreach (var component in pathComponents.Take(pathComponents.Length - 1))
|
||||
{
|
||||
if (!Utility.TomlTryGetValueCaseInsensitive(current, component, out var next) || next is not TomlTable nextTable)
|
||||
{
|
||||
resultValue = default;
|
||||
return false;
|
||||
}
|
||||
current = nextTable;
|
||||
}
|
||||
if (!Utility.TomlTryGetValueCaseInsensitive(current, pathComponents.Last(), out var value))
|
||||
{
|
||||
resultValue = default;
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
resultValue = Utility.ParseTomlValue<T>(value);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Utility.Log($"Failed to parse value at {path}: {e.Message}");
|
||||
resultValue = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string ToToml()
|
||||
{
|
||||
return root.SerializedValue;
|
||||
}
|
||||
}
|
||||
7
AquaMai/AquaMai.Config/FodyWeavers.xml
Normal file
7
AquaMai/AquaMai.Config/FodyWeavers.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ILMerge>
|
||||
<IncludeAssemblies>tomlet</IncludeAssemblies>
|
||||
<NamespacePrefix>$AquaMai.Config$_</NamespacePrefix>
|
||||
</ILMerge>
|
||||
</Weavers>
|
||||
111
AquaMai/AquaMai.Config/FodyWeavers.xsd
Normal file
111
AquaMai/AquaMai.Config/FodyWeavers.xsd
Normal file
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="ILMerge" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="IncludeAssemblies" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A regular expression matching the assembly names to include in merging.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="ExcludeAssemblies" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A regular expression matching the assembly names to exclude from merging.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="IncludeResources" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A regular expression matching the resource names to include in merging.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="ExcludeResources" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A regular expression matching the resource names to exclude from merging.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="HideImportedTypes" type="xs:boolean" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A switch to control whether the imported types are hidden (made private) or keep their visibility unchanged. Default is 'true'</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="NamespacePrefix" type="xs:string" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A string that is used as prefix for the namespace of the imported types.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="FullImport" type="xs:boolean" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A switch to control whether to import the full assemblies or only the referenced types. Default is 'false'</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="CompactMode" type="xs:boolean" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A switch to enable compacting of the target assembly by skipping properties, events and unused methods. Default is 'false'</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A regular expression matching the assembly names to include in merging.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A regular expression matching the assembly names to exclude from merging.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeResources" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A regular expression matching the resource names to include in merging.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeResources" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A regular expression matching the resource names to exclude from merging.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="HideImportedTypes" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A switch to control whether the imported types are hidden (made private) or keep their visibility unchanged. Default is 'true'</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="NamespacePrefix" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A string that is used as prefix for the namespace of the imported types.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="FullImport" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A switch to control whether to import the full assemblies or only the referenced types. Default is 'false'</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="CompactMode" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A switch to enable compacting of the target assembly by skipping properties, events and unused methods. Default is 'false'</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
58
AquaMai/AquaMai.Config/Migration/ConfigMigrationManager.cs
Normal file
58
AquaMai/AquaMai.Config/Migration/ConfigMigrationManager.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AquaMai.Config.Interfaces;
|
||||
|
||||
namespace AquaMai.Config.Migration;
|
||||
|
||||
public class ConfigMigrationManager : IConfigMigrationManager
|
||||
{
|
||||
public static readonly ConfigMigrationManager Instance = new();
|
||||
|
||||
private readonly Dictionary<string, IConfigMigration> migrationMap =
|
||||
new List<IConfigMigration>
|
||||
{
|
||||
new ConfigMigration_V1_0_V2_0()
|
||||
}.ToDictionary(m => m.FromVersion);
|
||||
|
||||
public readonly string latestVersion;
|
||||
|
||||
private ConfigMigrationManager()
|
||||
{
|
||||
latestVersion = migrationMap.Values
|
||||
.Select(m => m.ToVersion)
|
||||
.OrderByDescending(version =>
|
||||
{
|
||||
var versionParts = version.Split('.').Select(int.Parse).ToArray();
|
||||
return versionParts[0] * 100000 + versionParts[1];
|
||||
})
|
||||
.First();
|
||||
}
|
||||
|
||||
public IConfigView Migrate(IConfigView config)
|
||||
{
|
||||
var currentVersion = GetVersion(config);
|
||||
while (migrationMap.ContainsKey(currentVersion))
|
||||
{
|
||||
var migration = migrationMap[currentVersion];
|
||||
Utility.Log($"Migrating config from v{migration.FromVersion} to v{migration.ToVersion}");
|
||||
config = migration.Migrate(config);
|
||||
currentVersion = migration.ToVersion;
|
||||
}
|
||||
if (currentVersion != latestVersion)
|
||||
{
|
||||
throw new ArgumentException($"Could not migrate the config from v{currentVersion} to v{latestVersion}");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public string GetVersion(IConfigView config)
|
||||
{
|
||||
if (config.TryGetValue<string>("Version", out var version))
|
||||
{
|
||||
return version;
|
||||
}
|
||||
// Assume v1.0 if not found
|
||||
return "1.0";
|
||||
}
|
||||
}
|
||||
346
AquaMai/AquaMai.Config/Migration/ConfigMigration_V1_0_V2_0.cs
Normal file
346
AquaMai/AquaMai.Config/Migration/ConfigMigration_V1_0_V2_0.cs
Normal file
@@ -0,0 +1,346 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using AquaMai.Config.Types;
|
||||
|
||||
namespace AquaMai.Config.Migration;
|
||||
|
||||
public class ConfigMigration_V1_0_V2_0 : IConfigMigration
|
||||
{
|
||||
public string FromVersion => "1.0";
|
||||
public string ToVersion => "2.0";
|
||||
|
||||
public IConfigView Migrate(IConfigView src)
|
||||
{
|
||||
var dst = new ConfigView();
|
||||
|
||||
dst.SetValue("Version", ToVersion);
|
||||
|
||||
// UX (legacy)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.TestProof", "GameSystem.TestProof");
|
||||
if (src.GetValueOrDefault<bool>("UX.QuickSkip"))
|
||||
{
|
||||
// NOTE: UX.QuickSkip was a 4-in-1 large patch in earlier V1, then split since ModKeyMap was introduced.
|
||||
dst.SetValue("UX.OneKeyEntryEnd.Key", "Service");
|
||||
dst.SetValue("UX.OneKeyEntryEnd.LongPress", true);
|
||||
dst.SetValue("UX.OneKeyRetrySkip.RetryKey", "Service");
|
||||
dst.SetValue("UX.OneKeyRetrySkip.RetryLongPress", false);
|
||||
dst.SetValue("UX.OneKeyRetrySkip.SkipKey", "Select1P");
|
||||
dst.SetValue("UX.OneKeyRetrySkip.SkipLongPress", false);
|
||||
dst.EnsureDictionary("GameSystem.QuickRetry");
|
||||
}
|
||||
if (src.GetValueOrDefault<bool>("UX.HideSelfMadeCharts"))
|
||||
{
|
||||
dst.SetValue("UX.HideSelfMadeCharts.Key", "Service");
|
||||
dst.SetValue("UX.HideSelfMadeCharts.LongPress", false);
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadJacketPng", "GameSystem.Assets.LoadLocalImages");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SkipWarningScreen", "Tweaks.TimeSaving.SkipStartupWarning");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SkipToMusicSelection", "Tweaks.TimeSaving.EntryToMusicSelection");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SkipEventInfo", "Tweaks.TimeSaving.SkipEventInfo");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SelectionDetail", "UX.SelectionDetail");
|
||||
if (src.GetValueOrDefault<bool>("UX.CustomNoteSkin") ||
|
||||
src.GetValueOrDefault<bool>("UX.CustomSkins"))
|
||||
{
|
||||
dst.SetValue("Fancy.CustomSkins.SkinsDir", "LocalAssets/Skins");
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.JudgeDisplay4B", "Fancy.GamePlay.JudgeDisplay4B");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.CustomTrackStartDiff", "Fancy.CustomTrackStartDiff");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.TrackStartProcessTweak", "Fancy.GamePlay.TrackStartProcessTweak");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.DisableTrackStartTabs", "Fancy.GamePlay.DisableTrackStartTabs");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.RealisticRandomJudge", "Fancy.GamePlay.RealisticRandomJudge");
|
||||
|
||||
// Utils (legacy)
|
||||
if (src.GetValueOrDefault<bool>("Utils.Windowed") ||
|
||||
src.GetValueOrDefault<int>("Utils.Width") != 0 ||
|
||||
src.GetValueOrDefault<int>("Utils.Height") != 0)
|
||||
{
|
||||
// NOTE: the default "false, 0, 0" was effective earlier in V1, but won't be migrated as enabled in V2.
|
||||
MapValueOrDefaultToEntryValue(src, dst, "Utils.Windowed", "GameSystem.Window.Windowed", false);
|
||||
MapValueOrDefaultToEntryValue(src, dst, "Utils.Width", "GameSystem.Window.Width", 0);
|
||||
MapValueOrDefaultToEntryValue(src, dst, "Utils.Height", "GameSystem.Window.Height", 0);
|
||||
}
|
||||
if (src.GetValueOrDefault<bool>("Utils.PracticeMode") || src.GetValueOrDefault<bool>("Utils.PractiseMode")) // Typo of typo is the correct word
|
||||
{
|
||||
dst.SetValue("UX.PracticeMode.Key", "Test");
|
||||
dst.SetValue("UX.PracticeMode.LongPress", false);
|
||||
}
|
||||
|
||||
// Fix (legacy)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.BreakSlideJudgeBlink");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.BreakSlideJudgeBlink", "Fancy.GamePlay.BreakSlideJudgeBlink");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.FanJudgeFlip");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.FanJudgeFlip", "Fancy.GamePlay.FanJudgeFlip");
|
||||
// NOTE: This (FixCircleSlideJudge) was enabled by default in V1, but non-default in V2 since it has visual changes
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.SlideJudgeTweak", "Fancy.GamePlay.AlignCircleSlideJudgeDisplay");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.FixCircleSlideJudge", "Fancy.GamePlay.AlignCircleSlideJudgeDisplay");
|
||||
|
||||
// Performance (legacy)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Performance.ImproveLoadSpeed", "Tweaks.TimeSaving.SkipStartupDelays");
|
||||
|
||||
// TimeSaving (legacy)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ShowNetErrorDetail", "Utils.ShowNetErrorDetail");
|
||||
|
||||
// UX
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.Locale", "General.Locale", "");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SinglePlayer", "GameSystem.SinglePlayer");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.HideMask", "Fancy.HideMask");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadAssetsPng", "GameSystem.Assets.LoadLocalImages");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadAssetBundleWithoutManifest", "GameSystem.Assets.LoadAssetBundleWithoutManifest");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.RandomBgm", "Fancy.RandomBgm");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.DemoMaster", "Fancy.DemoMaster");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.ExtendTimer", "GameSystem.DisableTimeout");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.ImmediateSave", "UX.ImmediateSave");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.LoadLocalBga", "GameSystem.Assets.UseJacketAsDummyMovie");
|
||||
if (src.GetValueOrDefault<bool>("UX.CustomFont"))
|
||||
{
|
||||
dst.SetValue("GameSystem.Assets.Fonts.Paths", "LocalAssets/font.ttf");
|
||||
dst.SetValue("GameSystem.Assets.Fonts.AddAsFallback", false);
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.TouchToButtonInput", "GameSystem.TouchToButtonInput");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.HideHanabi", "Fancy.GamePlay.HideHanabi");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.SlideFadeInTweak", "Fancy.GamePlay.SlideFadeInTweak");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "UX.JudgeAccuracyInfo", "UX.JudgeAccuracyInfo");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.CustomVersionString", "Fancy.CustomVersionString.VersionString", "");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.CustomPlaceName", "Fancy.CustomPlaceName.PlaceName", "");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.ExecOnIdle", "Fancy.Triggers.ExecOnIdle", "");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "UX.ExecOnEntry", "Fancy.Triggers.ExecOnEntry", "");
|
||||
|
||||
// Cheat
|
||||
var unlockTickets = src.GetValueOrDefault<bool>("Cheat.TicketUnlock");
|
||||
var unlockMaps = src.GetValueOrDefault<bool>("Cheat.MapUnlock");
|
||||
var unlockUtage = src.GetValueOrDefault<bool>("Cheat.UnlockUtage");
|
||||
if (unlockTickets ||
|
||||
unlockMaps ||
|
||||
unlockUtage)
|
||||
{
|
||||
dst.SetValue("GameSystem.Unlock.Tickets", unlockTickets);
|
||||
dst.SetValue("GameSystem.Unlock.Maps", unlockMaps);
|
||||
dst.SetValue("GameSystem.Unlock.Utage", unlockUtage);
|
||||
}
|
||||
|
||||
// Fix
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.SkipVersionCheck", "Tweaks.SkipUserVersionCheck");
|
||||
if (!src.GetValueOrDefault<bool>("Fix.RemoveEncryption"))
|
||||
{
|
||||
dst.SetValue("GameSystem.RemoveEncryption.Disabled", true); // Enabled by default in V2
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.ForceAsServer", "GameSettings.ForceAsServer");
|
||||
if (src.GetValueOrDefault<bool>("Fix.ForceFreePlay"))
|
||||
{
|
||||
dst.SetValue("GameSettings.CreditConfig.IsFreePlay", true);
|
||||
}
|
||||
if (src.GetValueOrDefault<bool>("Fix.ForcePaidPlay"))
|
||||
{
|
||||
dst.SetValue("GameSettings.CreditConfig.IsFreePlay", false);
|
||||
dst.SetValue("GameSettings.CreditConfig.LockCredits", 24);
|
||||
}
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "Fix.ExtendNotesPool", "Fancy.GamePlay.ExtendNotesPool.Count", 0);
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.FrameRateLock", "Tweaks.LockFrameRate");
|
||||
if (src.GetValueOrDefault<bool>("Font.FontFix") &&
|
||||
!src.GetValueOrDefault<bool>("UX.CustomFont"))
|
||||
{
|
||||
dst.SetValue("GameSystem.Assets.Fonts.Paths", "%SYSTEMROOT%/Fonts/msyhbd.ttc");
|
||||
dst.SetValue("GameSystem.Assets.Fonts.AddAsFallback", true);
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.RealisticRandomJudge", "Fancy.GamePlay.RealisticRandomJudge");
|
||||
if (src.GetValueOrDefault<bool>("UX.SinglePlayer"))
|
||||
{
|
||||
if (src.TryGetValue("Fix.HanabiFix", out bool hanabiFix))
|
||||
{
|
||||
// If it's enabled or disabled explicitly, use the value, otherwise left empty use the default V2 value (enabled).
|
||||
dst.SetValue("GameSystem.SinglePlayer.FixHanabi", hanabiFix);
|
||||
}
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.IgnoreAimeServerError", "Tweaks.IgnoreAimeServerError");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Fix.TouchResetAfterTrack", "Tweaks.ResetTouchAfterTrack");
|
||||
|
||||
// Utils
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.LogUserId", "Utils.LogUserId");
|
||||
MapValueToEntryValueIfNonNullOrDefault<double>(src, dst, "Utils.JudgeAdjustA", "GameSettings.JudgeAdjust.A", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault<double>(src, dst, "Utils.JudgeAdjustB", "GameSettings.JudgeAdjust.B", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.TouchDelay", "GameSettings.JudgeAdjust.TouchDelay", 0);
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.SelectionDetail", "UX.SelectionDetail");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.ShowNetErrorDetail", "Utils.ShowNetErrorDetail");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.ShowErrorLog", "Utils.ShowErrorLog");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Utils.FrameRateDisplay", "Utils.DisplayFrameRate");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "Utils.TouchPanelBaudRate", "GameSystem.TouchPanelBaudRate.BaudRate", 0);
|
||||
|
||||
// TimeSaving
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipWarningScreen", "Tweaks.TimeSaving.SkipStartupWarning");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ImproveLoadSpeed", "Tweaks.TimeSaving.SkipStartupDelays");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipToMusicSelection", "Tweaks.TimeSaving.EntryToMusicSelection");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipEventInfo", "Tweaks.TimeSaving.SkipEventInfo");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.IWontTapOrSlideVigorously", "Tweaks.TimeSaving.IWontTapOrSlideVigorously");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipGameOverScreen", "Tweaks.TimeSaving.SkipGoodbyeScreen");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.SkipTrackStart", "Tweaks.TimeSaving.SkipTrackStart");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "TimeSaving.ShowQuickEndPlay", "UX.QuickEndPlay");
|
||||
|
||||
// Visual
|
||||
if (src.GetValueOrDefault<bool>("Visual.CustomSkins"))
|
||||
{
|
||||
dst.SetValue("Fancy.CustomSkins.SkinsDir", "LocalAssets/Skins");
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.JudgeDisplay4B", "Fancy.GamePlay.JudgeDisplay4B");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.CustomTrackStartDiff", "Fancy.CustomTrackStartDiff");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.TrackStartProcessTweak", "Fancy.GamePlay.TrackStartProcessTweak");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.DisableTrackStartTabs", "Fancy.GamePlay.DisableTrackStartTabs");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.FanJudgeFlip", "Fancy.GamePlay.FanJudgeFlip");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.BreakSlideJudgeBlink", "Fancy.GamePlay.BreakSlideJudgeBlink");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.SlideArrowAnimation", "Fancy.GamePlay.SlideArrowAnimation");
|
||||
MapBooleanTrueToSectionEnable(src, dst, "Visual.SlideLayerReverse", "Fancy.GamePlay.SlideLayerReverse");
|
||||
|
||||
// ModKeyMap
|
||||
var keyQuickSkip = src.GetValueOrDefault("ModKeyMap.QuickSkip", "None");
|
||||
var keyInGameRetry = src.GetValueOrDefault("ModKeyMap.InGameRetry", "None");
|
||||
var keyInGameSkip = src.GetValueOrDefault("ModKeyMap.InGameSkip", "None");
|
||||
var keyPractiseMode = src.GetValueOrDefault("ModKeyMap.PractiseMode", "None");
|
||||
var keyHideSelfMadeCharts = src.GetValueOrDefault("ModKeyMap.HideSelfMadeCharts", "None");
|
||||
if (keyQuickSkip != "None")
|
||||
{
|
||||
dst.SetValue("UX.OneKeyEntryEnd.Key", keyQuickSkip);
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.QuickSkipLongPress", "UX.OneKeyEntryEnd.LongPress");
|
||||
}
|
||||
if (keyInGameRetry != "None" || keyInGameSkip != "None")
|
||||
{
|
||||
dst.SetValue("UX.OneKeyRetrySkip.RetryKey", keyInGameRetry);
|
||||
if (keyInGameRetry != "None")
|
||||
{
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.InGameRetryLongPress", "UX.OneKeyRetrySkip.RetryLongPress");
|
||||
}
|
||||
dst.SetValue("UX.OneKeyRetrySkip.SkipKey", keyInGameSkip);
|
||||
if (keyInGameSkip != "None")
|
||||
{
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.InGameSkipLongPress", "UX.OneKeyRetrySkip.SkipLongPress");
|
||||
}
|
||||
}
|
||||
if (keyPractiseMode != "None")
|
||||
{
|
||||
dst.SetValue("UX.PracticeMode.Key", keyPractiseMode);
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.PractiseModeLongPress", "UX.PracticeMode.LongPress");
|
||||
}
|
||||
if (keyHideSelfMadeCharts != "None")
|
||||
{
|
||||
dst.SetValue("UX.HideSelfMadeCharts.Key", keyHideSelfMadeCharts);
|
||||
MapValueToEntryValueIfNonNull<bool>(src, dst, "ModKeyMap.HideSelfMadeChartsLongPress", "UX.HideSelfMadeCharts.LongPress");
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "ModKeyMap.EnableNativeQuickRetry", "GameSystem.QuickRetry");
|
||||
if (src.TryGetValue<string>("ModKeyMap.TestMode", out var testMode) &&
|
||||
testMode != "" &&
|
||||
testMode != "Test")
|
||||
{
|
||||
dst.SetValue("DeprecationWarning.v1_0_ModKeyMap_TestMode", true);
|
||||
}
|
||||
MapBooleanTrueToSectionEnable(src, dst, "ModKeyMap.TestModeLongPress", "GameSystem.TestProof");
|
||||
|
||||
// WindowState
|
||||
if (src.GetValueOrDefault<bool>("WindowState.Enable"))
|
||||
{
|
||||
MapValueOrDefaultToEntryValue(src, dst, "WindowState.Windowed", "GameSystem.Window.Windowed", false);
|
||||
MapValueOrDefaultToEntryValue(src, dst, "WindowState.Width", "GameSystem.Window.Width", 0);
|
||||
MapValueOrDefaultToEntryValue(src, dst, "WindowState.Height", "GameSystem.Window.Height", 0);
|
||||
}
|
||||
|
||||
// CustomCameraId
|
||||
if (src.GetValueOrDefault<bool>("CustomCameraId.Enable"))
|
||||
{
|
||||
dst.EnsureDictionary("GameSystem.CustomCameraId");
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.PrintCameraList", "GameSystem.CustomCameraId.PrintCameraList", false);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.LeftQrCamera", "GameSystem.CustomCameraId.LeftQrCamera", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.RightQrCamera", "GameSystem.CustomCameraId.RightQrCamera", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.PhotoCamera", "GameSystem.CustomCameraId.PhotoCamera", 0);
|
||||
MapValueToEntryValueIfNonNullOrDefault(src, dst, "CustomCameraId.ChimeCamera", "GameSystem.CustomCameraId.ChimeCamera", 0);
|
||||
}
|
||||
|
||||
// TouchSensitivity
|
||||
if (src.GetValueOrDefault<bool>("TouchSensitivity.Enable"))
|
||||
{
|
||||
dst.EnsureDictionary("GameSettings.TouchSensitivity");
|
||||
var areas = new[]
|
||||
{
|
||||
"A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8",
|
||||
"B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8",
|
||||
"C1", "C2",
|
||||
"D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8",
|
||||
"E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8",
|
||||
};
|
||||
foreach (var area in areas)
|
||||
{
|
||||
MapValueToEntryValueIfNonNull<int>(src, dst, $"TouchSensitivity.{area}", $"GameSettings.TouchSensitivity.{area}");
|
||||
}
|
||||
}
|
||||
|
||||
// CustomKeyMap
|
||||
if (src.GetValueOrDefault<bool>("CustomKeyMap.Enable"))
|
||||
{
|
||||
dst.EnsureDictionary("GameSystem.KeyMap");
|
||||
var keys = new[]
|
||||
{
|
||||
"Test", "Service",
|
||||
"Button1_1P", "Button3_1P", "Button4_1P", "Button2_1P", "Button5_1P", "Button6_1P", "Button7_1P", "Button8_1P",
|
||||
"Select_1P",
|
||||
"Button1_2P", "Button2_2P", "Button3_2P", "Button4_2P", "Button5_2P", "Button6_2P", "Button7_2P", "Button8_2P",
|
||||
"Select_2P"
|
||||
};
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (src.TryGetValue<string>($"CustomKeyMap.{key}", out var value) &&
|
||||
Enum.TryParse<KeyCodeID>(value, out var keyCode))
|
||||
{
|
||||
dst.SetValue($"GameSystem.KeyMap.{key}", keyCode.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MaimaiDX2077 (WTF is the name?)
|
||||
MapBooleanTrueToSectionEnable(src, dst, "MaimaiDX2077.CustomNoteTypePatch", "Fancy.GamePlay.CustomNoteTypes");
|
||||
|
||||
// Default enabled in V2
|
||||
dst.EnsureDictionary("GameSystem.RemoveEncryption");
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
// An value in the old config maps to an entry value in the new config.
|
||||
// Any existing value, including zero, is valid.
|
||||
private void MapValueToEntryValueIfNonNull<T>(IConfigView src, ConfigView dst, string srcKey, string dstKey)
|
||||
{
|
||||
if (src.TryGetValue<T>(srcKey, out var value))
|
||||
{
|
||||
dst.SetValue(dstKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
// An value in the old config maps to an entry value in the new config.
|
||||
// Null or default value is ignored.
|
||||
private void MapValueToEntryValueIfNonNullOrDefault<T>(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue)
|
||||
{
|
||||
if (src.TryGetValue<T>(srcKey, out var value) && !EqualityComparer<T>.Default.Equals(value, defaultValue))
|
||||
{
|
||||
dst.SetValue(dstKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
// An value in the old config maps to an entry value in the new config.
|
||||
// Null value is replaced with a default value.
|
||||
private void MapValueOrDefaultToEntryValue<T>(IConfigView src, ConfigView dst, string srcKey, string dstKey, T defaultValue)
|
||||
{
|
||||
if (src.TryGetValue<T>(srcKey, out var value))
|
||||
{
|
||||
dst.SetValue(dstKey, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
dst.SetValue(dstKey, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
// An boolean value in the old config maps to a default-off section's enable in the new config.
|
||||
private void MapBooleanTrueToSectionEnable(IConfigView src, ConfigView dst, string srcKey, string dstKey)
|
||||
{
|
||||
if (src.GetValueOrDefault<bool>(srcKey))
|
||||
{
|
||||
dst.EnsureDictionary(dstKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
AquaMai/AquaMai.Config/Migration/IConfigMigration.cs
Normal file
10
AquaMai/AquaMai.Config/Migration/IConfigMigration.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using AquaMai.Config.Interfaces;
|
||||
|
||||
namespace AquaMai.Config.Migration;
|
||||
|
||||
public interface IConfigMigration
|
||||
{
|
||||
public string FromVersion { get; }
|
||||
public string ToVersion { get; }
|
||||
public IConfigView Migrate(IConfigView config);
|
||||
}
|
||||
4
AquaMai/AquaMai.Config/Polyfills.cs
Normal file
4
AquaMai/AquaMai.Config/Polyfills.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit {}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
|
||||
namespace AquaMai.Config.Reflection;
|
||||
|
||||
public class MonoCecilReflectionProvider : IReflectionProvider
|
||||
{
|
||||
public record ReflectionField(
|
||||
string Name,
|
||||
Type FieldType,
|
||||
object Value,
|
||||
IDictionary<Type, object> Attributes) : IReflectionField
|
||||
{
|
||||
public object Value { get; set; } = Value;
|
||||
|
||||
public T GetCustomAttribute<T>() where T : Attribute => Attributes.TryGetValue(typeof(T), out var value) ? (T)value : null;
|
||||
public object GetValue(object obj) => Value;
|
||||
public void SetValue(object obj, object value) => Value = value;
|
||||
}
|
||||
|
||||
public record ReflectionType(
|
||||
string FullName,
|
||||
string Namespace,
|
||||
IReflectionField[] Fields,
|
||||
IDictionary<Type, object> Attributes) : IReflectionType
|
||||
{
|
||||
public T GetCustomAttribute<T>() where T : Attribute => Attributes.TryGetValue(typeof(T), out var value) ? (T)value : null;
|
||||
public IReflectionField[] GetFields(BindingFlags bindingAttr) => Fields;
|
||||
}
|
||||
|
||||
private static readonly Type[] attributeTypes =
|
||||
[
|
||||
typeof(ConfigCollapseNamespaceAttribute),
|
||||
typeof(ConfigSectionAttribute),
|
||||
typeof(ConfigEntryAttribute),
|
||||
];
|
||||
|
||||
private readonly IReflectionType[] reflectionTypes = [];
|
||||
private readonly Dictionary<string, Dictionary<string, object>> enums = [];
|
||||
|
||||
public IReflectionType[] GetTypes() => reflectionTypes;
|
||||
public Dictionary<string, object> GetEnum(string enumName) => enums[enumName];
|
||||
|
||||
public MonoCecilReflectionProvider(AssemblyDefinition assembly)
|
||||
{
|
||||
reflectionTypes = assembly.MainModule.Types.Select(cType => {
|
||||
var typeAttributes = InstantiateAttributes(cType.CustomAttributes);
|
||||
var fields = cType.Fields.Select(cField => {
|
||||
try
|
||||
{
|
||||
var fieldAttributes = InstantiateAttributes(cField.CustomAttributes);
|
||||
if (fieldAttributes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var type = GetRuntimeType(cField.FieldType);
|
||||
var defaultValue = GetFieldDefaultValue(cType, cField, type);
|
||||
return new ReflectionField(cField.Name, type, defaultValue, fieldAttributes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
return null;
|
||||
}).Where(field => field != null).ToArray();
|
||||
return new ReflectionType(cType.FullName, cType.Namespace, fields, typeAttributes);
|
||||
}).ToArray();
|
||||
enums = assembly.MainModule.Types
|
||||
.Where(cType => cType.IsEnum)
|
||||
.ToDictionary(cType =>
|
||||
cType.FullName,
|
||||
cType => cType.Fields
|
||||
.Where(cField => cField.IsPublic && cField.IsStatic && cField.Constant != null)
|
||||
.ToDictionary(cField => cField.Name, cField => cField.Constant));
|
||||
}
|
||||
|
||||
private Dictionary<Type, object> InstantiateAttributes(ICollection<CustomAttribute> attribute) =>
|
||||
attribute
|
||||
.Select(InstantiateAttribute)
|
||||
.Where(a => a != null)
|
||||
.ToDictionary(a => a.GetType(), a => a);
|
||||
|
||||
private object InstantiateAttribute(CustomAttribute attribute) =>
|
||||
attributeTypes.FirstOrDefault(t => t.FullName == attribute.AttributeType.FullName) switch
|
||||
{
|
||||
Type type => Activator.CreateInstance(type,
|
||||
attribute.Constructor.Parameters
|
||||
.Select((parameter, i) =>
|
||||
{
|
||||
var runtimeType = GetRuntimeType(parameter.ParameterType);
|
||||
var value = attribute.ConstructorArguments[i].Value;
|
||||
if (runtimeType.IsEnum)
|
||||
{
|
||||
return Enum.Parse(runtimeType, value.ToString());
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.ToArray()),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private Type GetRuntimeType(TypeReference typeReference) {
|
||||
if (typeReference.IsGenericInstance)
|
||||
{
|
||||
var genericInstance = (GenericInstanceType)typeReference;
|
||||
var genericType = GetRuntimeType(genericInstance.ElementType);
|
||||
var genericArguments = genericInstance.GenericArguments.Select(GetRuntimeType).ToArray();
|
||||
return genericType.MakeGenericType(genericArguments);
|
||||
}
|
||||
|
||||
var type = Type.GetType(typeReference.FullName);
|
||||
if (type == null)
|
||||
{
|
||||
throw new TypeLoadException($"Type {typeReference.FullName} not found.");
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private static object GetFieldDefaultValue(TypeDefinition cType, FieldDefinition cField, Type fieldType)
|
||||
{
|
||||
object defaultValue = null;
|
||||
var cctor = cType.Methods.SingleOrDefault(m => m.Name == ".cctor");
|
||||
if (cctor != null)
|
||||
{
|
||||
var store = cctor.Body.Instructions.SingleOrDefault(i => i.OpCode == OpCodes.Stsfld && i.Operand == cField);
|
||||
if (store != null)
|
||||
{
|
||||
var loadOperand = ParseConstantLoadOperand(store.Previous);
|
||||
if (fieldType == typeof(bool))
|
||||
{
|
||||
defaultValue = Convert.ToBoolean(loadOperand);
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultValue = loadOperand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultValue == null && cField.HasDefault)
|
||||
{
|
||||
throw new InvalidOperationException($"Field {cType.FullName}.{cField.Name} has default value but no .cctor stsfld instruction.");
|
||||
}
|
||||
defaultValue ??= GetDefaultValue(fieldType);
|
||||
|
||||
if (fieldType.IsEnum)
|
||||
{
|
||||
var enumType = fieldType.GetEnumUnderlyingType();
|
||||
// Assume casting is safe since we're getting the default value from the field
|
||||
var castedValue = Convert.ChangeType(defaultValue, enumType);
|
||||
if (Enum.IsDefined(fieldType, castedValue))
|
||||
{
|
||||
return Enum.ToObject(fieldType, castedValue);
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static object ParseConstantLoadOperand(Instruction instruction)
|
||||
{
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_M1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_1)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_2)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_3)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_4)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_5)
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_6)
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_7)
|
||||
{
|
||||
return 7;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_8)
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4_S)
|
||||
{
|
||||
return Convert.ToInt32((sbyte)instruction.Operand);
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I4)
|
||||
{
|
||||
return (int)instruction.Operand;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_I8)
|
||||
{
|
||||
return (long)instruction.Operand;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_R4)
|
||||
{
|
||||
return (float)instruction.Operand;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldc_R8)
|
||||
{
|
||||
return (double)instruction.Operand;
|
||||
}
|
||||
if (instruction.OpCode == OpCodes.Ldstr)
|
||||
{
|
||||
return (string)instruction.Operand;
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentMethod = MethodBase.GetCurrentMethod();
|
||||
throw new NotImplementedException($"Unsupported constant load: {instruction}. Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetDefaultValue(Type type)
|
||||
{
|
||||
if (type.IsValueType)
|
||||
{
|
||||
return Activator.CreateInstance(type);
|
||||
}
|
||||
else if (type == typeof(string))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
178
AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs
Normal file
178
AquaMai/AquaMai.Config/Reflection/ReflectionManager.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using AquaMai.Config.Attributes;
|
||||
using AquaMai.Config.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace AquaMai.Config.Reflection;
|
||||
|
||||
public class ReflectionManager : IReflectionManager
|
||||
{
|
||||
public record Entry : IReflectionManager.IEntry
|
||||
{
|
||||
public string Path { get; init; }
|
||||
public string Name { get; init; }
|
||||
public IReflectionField Field { get; init; }
|
||||
public ConfigEntryAttribute Attribute { get; init; }
|
||||
}
|
||||
|
||||
public record Section : IReflectionManager.ISection
|
||||
{
|
||||
public string Path { get; init; }
|
||||
public IReflectionType Type { get; init; }
|
||||
public ConfigSectionAttribute Attribute { get; init; }
|
||||
public List<Entry> entries;
|
||||
public List<IReflectionManager.IEntry> Entries => entries.Cast<IReflectionManager.IEntry>().ToList();
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, Section> sections = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Entry> entries = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Section> sectionsByFullName = [];
|
||||
|
||||
public ReflectionManager(IReflectionProvider reflectionProvider)
|
||||
{
|
||||
var prefix = "AquaMai.Mods.";
|
||||
var types = reflectionProvider.GetTypes().Where(t => t.FullName.StartsWith(prefix));
|
||||
var collapsedNamespaces = new HashSet<string>();
|
||||
foreach (var type in types)
|
||||
{
|
||||
var sectionAttribute = type.GetCustomAttribute<ConfigSectionAttribute>();
|
||||
if (sectionAttribute == null) continue;
|
||||
if (collapsedNamespaces.Contains(type.Namespace))
|
||||
{
|
||||
throw new Exception($"Collapsed namespace {type.Namespace} contains multiple sections");
|
||||
}
|
||||
var path = type.FullName.Substring(prefix.Length);
|
||||
if (type.GetCustomAttribute<ConfigCollapseNamespaceAttribute>() != null)
|
||||
{
|
||||
var separated = path.Split('.');
|
||||
if (separated[separated.Length - 2] != separated[separated.Length - 1])
|
||||
{
|
||||
throw new Exception($"Type {type.FullName} is not collapsable");
|
||||
}
|
||||
path = string.Join(".", separated.Take(separated.Length - 1));
|
||||
collapsedNamespaces.Add(type.Namespace);
|
||||
}
|
||||
|
||||
var sectionEntries = new List<Entry>();
|
||||
foreach (var field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
var entryAttribute = field.GetCustomAttribute<ConfigEntryAttribute>();
|
||||
if (entryAttribute == null) continue;
|
||||
var transformedName = Utility.ToPascalCase(field.Name);
|
||||
var entryPath = $"{path}.{transformedName}";
|
||||
var entry = new Entry()
|
||||
{
|
||||
Path = entryPath,
|
||||
Name = transformedName,
|
||||
Field = field,
|
||||
Attribute = entryAttribute
|
||||
};
|
||||
sectionEntries.Add(entry);
|
||||
entries.Add(entryPath, entry);
|
||||
}
|
||||
|
||||
var section = new Section()
|
||||
{
|
||||
Path = path,
|
||||
Type = type,
|
||||
Attribute = sectionAttribute,
|
||||
entries = sectionEntries
|
||||
};
|
||||
sections.Add(path, section);
|
||||
sectionsByFullName.Add(type.FullName, section);
|
||||
}
|
||||
|
||||
var order = reflectionProvider.GetEnum("AquaMai.Mods.SetionNameOrder");
|
||||
sections = sections
|
||||
.OrderBy(x => x.Key)
|
||||
.OrderBy(x =>
|
||||
{
|
||||
var parts = x.Key.Split('.');
|
||||
for (int i = parts.Length; i > 0; i--)
|
||||
{
|
||||
var key = string.Join("_", parts.Take(i));
|
||||
if (order.TryGetValue(key, out var value))
|
||||
{
|
||||
return (int)value;
|
||||
}
|
||||
}
|
||||
Utility.Log($"Section {x.Key} has no order defined, defaulting to int.MaxValue");
|
||||
return int.MaxValue;
|
||||
})
|
||||
.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public IEnumerable<Section> SectionValues => sections.Values;
|
||||
public IEnumerable<IReflectionManager.ISection> Sections => sections.Values.Cast<IReflectionManager.ISection>();
|
||||
|
||||
public IEnumerable<Entry> EntryValues => entries.Values;
|
||||
public IEnumerable<IReflectionManager.IEntry> Entries => entries.Values.Cast<IReflectionManager.IEntry>();
|
||||
|
||||
public bool ContainsSection(string path)
|
||||
{
|
||||
return sections.ContainsKey(path);
|
||||
}
|
||||
|
||||
public bool TryGetSection(string path, out IReflectionManager.ISection section)
|
||||
{
|
||||
if (sections.TryGetValue(path, out var sectionValue))
|
||||
{
|
||||
section = sectionValue;
|
||||
return true;
|
||||
}
|
||||
section = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetSection(Type type, out IReflectionManager.ISection section)
|
||||
{
|
||||
bool result = sectionsByFullName.TryGetValue(type.FullName, out var sectionValue);
|
||||
section = sectionValue;
|
||||
return result;
|
||||
}
|
||||
|
||||
public IReflectionManager.ISection GetSection(string path)
|
||||
{
|
||||
if (!TryGetSection(path, out var section))
|
||||
{
|
||||
throw new KeyNotFoundException($"Section {path} not found");
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
public IReflectionManager.ISection GetSection(Type type)
|
||||
{
|
||||
if (!TryGetSection(type.FullName, out var section))
|
||||
{
|
||||
throw new KeyNotFoundException($"Section {type.FullName} not found");
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
public bool ContainsEntry(string path)
|
||||
{
|
||||
return entries.ContainsKey(path);
|
||||
}
|
||||
|
||||
public bool TryGetEntry(string path, out IReflectionManager.IEntry entry)
|
||||
{
|
||||
if (entries.TryGetValue(path, out var entryValue))
|
||||
{
|
||||
entry = entryValue;
|
||||
return true;
|
||||
}
|
||||
entry = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public IReflectionManager.IEntry GetEntry(string path)
|
||||
{
|
||||
if (!TryGetEntry(path, out var entry))
|
||||
{
|
||||
throw new KeyNotFoundException($"Entry {path} not found");
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using AquaMai.Config.Interfaces;
|
||||
|
||||
namespace AquaMai.Config.Reflection;
|
||||
|
||||
public class SystemReflectionProvider(Assembly assembly) : IReflectionProvider
|
||||
{
|
||||
public class ReflectionField(FieldInfo field) : IReflectionField
|
||||
{
|
||||
public FieldInfo UnderlyingField { get; } = field;
|
||||
|
||||
public string Name => UnderlyingField.Name;
|
||||
public Type FieldType => UnderlyingField.FieldType;
|
||||
public T GetCustomAttribute<T>() where T : Attribute => UnderlyingField.GetCustomAttribute<T>();
|
||||
public object GetValue(object obj) => UnderlyingField.GetValue(obj);
|
||||
public void SetValue(object obj, object value) => UnderlyingField.SetValue(obj, value);
|
||||
}
|
||||
|
||||
public class ReflectionType(Type type) : IReflectionType
|
||||
{
|
||||
public Type UnderlyingType { get; } = type;
|
||||
|
||||
public string FullName => UnderlyingType.FullName;
|
||||
public string Namespace => UnderlyingType.Namespace;
|
||||
public T GetCustomAttribute<T>() where T : Attribute => UnderlyingType.GetCustomAttribute<T>();
|
||||
public IReflectionField[] GetFields(BindingFlags bindingAttr) => Array.ConvertAll(UnderlyingType.GetFields(bindingAttr), f => new ReflectionField(f));
|
||||
}
|
||||
|
||||
public Assembly UnderlyingAssembly { get; } = assembly;
|
||||
|
||||
public IReflectionType[] GetTypes() => Array.ConvertAll(UnderlyingAssembly.GetTypes(), t => new ReflectionType(t));
|
||||
|
||||
public Dictionary<string, object> GetEnum(string enumName)
|
||||
{
|
||||
var enumType = UnderlyingAssembly.GetType(enumName);
|
||||
if (enumType == null) return null;
|
||||
var enumValues = Enum.GetValues(enumType);
|
||||
var enumDict = new Dictionary<string, object>();
|
||||
foreach (var enumValue in enumValues)
|
||||
{
|
||||
enumDict.Add(enumValue.ToString(), enumValue);
|
||||
}
|
||||
return enumDict;
|
||||
}
|
||||
}
|
||||
146
AquaMai/AquaMai.Config/Types/KeyCodeID.cs
Normal file
146
AquaMai/AquaMai.Config/Types/KeyCodeID.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
namespace AquaMai.Config.Types;
|
||||
|
||||
public enum KeyCodeID
|
||||
{
|
||||
None,
|
||||
Backspace,
|
||||
Tab,
|
||||
Clear,
|
||||
Return,
|
||||
Pause,
|
||||
Escape,
|
||||
Space,
|
||||
Exclaim,
|
||||
DoubleQuote,
|
||||
Hash,
|
||||
Dollar,
|
||||
Ampersand,
|
||||
Quote,
|
||||
LeftParen,
|
||||
RightParen,
|
||||
Asterisk,
|
||||
Plus,
|
||||
Comma,
|
||||
Minus,
|
||||
Period,
|
||||
Slash,
|
||||
Alpha0,
|
||||
Alpha1,
|
||||
Alpha2,
|
||||
Alpha3,
|
||||
Alpha4,
|
||||
Alpha5,
|
||||
Alpha6,
|
||||
Alpha7,
|
||||
Alpha8,
|
||||
Alpha9,
|
||||
Colon,
|
||||
Semicolon,
|
||||
Less,
|
||||
Equals,
|
||||
Greater,
|
||||
Question,
|
||||
At,
|
||||
LeftBracket,
|
||||
Backslash,
|
||||
RightBracket,
|
||||
Caret,
|
||||
Underscore,
|
||||
BackQuote,
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
E,
|
||||
F,
|
||||
G,
|
||||
H,
|
||||
I,
|
||||
J,
|
||||
K,
|
||||
L,
|
||||
M,
|
||||
N,
|
||||
O,
|
||||
P,
|
||||
Q,
|
||||
R,
|
||||
S,
|
||||
T,
|
||||
U,
|
||||
V,
|
||||
W,
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
Delete,
|
||||
Keypad0,
|
||||
Keypad1,
|
||||
Keypad2,
|
||||
Keypad3,
|
||||
Keypad4,
|
||||
Keypad5,
|
||||
Keypad6,
|
||||
Keypad7,
|
||||
Keypad8,
|
||||
Keypad9,
|
||||
KeypadPeriod,
|
||||
KeypadDivide,
|
||||
KeypadMultiply,
|
||||
KeypadMinus,
|
||||
KeypadPlus,
|
||||
KeypadEnter,
|
||||
KeypadEquals,
|
||||
UpArrow,
|
||||
DownArrow,
|
||||
RightArrow,
|
||||
LeftArrow,
|
||||
Insert,
|
||||
Home,
|
||||
End,
|
||||
PageUp,
|
||||
PageDown,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
F13,
|
||||
F14,
|
||||
F15,
|
||||
Numlock,
|
||||
CapsLock,
|
||||
ScrollLock,
|
||||
RightShift,
|
||||
LeftShift,
|
||||
RightControl,
|
||||
LeftControl,
|
||||
RightAlt,
|
||||
LeftAlt,
|
||||
RightCommand,
|
||||
RightApple,
|
||||
LeftCommand,
|
||||
LeftApple,
|
||||
LeftWindows,
|
||||
RightWindows,
|
||||
AltGr,
|
||||
Help,
|
||||
Print,
|
||||
SysReq,
|
||||
Break,
|
||||
Menu,
|
||||
Mouse0,
|
||||
Mouse1,
|
||||
Mouse2,
|
||||
Mouse3,
|
||||
Mouse4,
|
||||
Mouse5,
|
||||
Mouse6,
|
||||
}
|
||||
53
AquaMai/AquaMai.Config/Types/KeyCodeOrName.cs
Normal file
53
AquaMai/AquaMai.Config/Types/KeyCodeOrName.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace AquaMai.Config.Types;
|
||||
|
||||
public enum KeyCodeOrName
|
||||
{
|
||||
None,
|
||||
Alpha0,
|
||||
Alpha1,
|
||||
Alpha2,
|
||||
Alpha3,
|
||||
Alpha4,
|
||||
Alpha5,
|
||||
Alpha6,
|
||||
Alpha7,
|
||||
Alpha8,
|
||||
Alpha9,
|
||||
Keypad0,
|
||||
Keypad1,
|
||||
Keypad2,
|
||||
Keypad3,
|
||||
Keypad4,
|
||||
Keypad5,
|
||||
Keypad6,
|
||||
Keypad7,
|
||||
Keypad8,
|
||||
Keypad9,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
Insert,
|
||||
Delete,
|
||||
Home,
|
||||
End,
|
||||
PageUp,
|
||||
PageDown,
|
||||
UpArrow,
|
||||
DownArrow,
|
||||
LeftArrow,
|
||||
RightArrow,
|
||||
|
||||
Select1P,
|
||||
Select2P,
|
||||
Service,
|
||||
Test,
|
||||
}
|
||||
183
AquaMai/AquaMai.Config/Utility.cs
Normal file
183
AquaMai/AquaMai.Config/Utility.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Tomlet.Models;
|
||||
|
||||
namespace AquaMai.Config;
|
||||
|
||||
public static class Utility
|
||||
{
|
||||
public static Action<string> LogFunction = Console.WriteLine;
|
||||
|
||||
public static bool IsTruty(TomlValue value, string path = null)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
TomlBoolean boolean => boolean.Value,
|
||||
TomlLong @long => @long.Value != 0,
|
||||
_ => throw new ArgumentException(
|
||||
path == null
|
||||
? $"Non-boolish TOML type {value.GetType().Name} value: {value}"
|
||||
: $"When parsing {path}, got non-boolish TOML type {value.GetType().Name} value: {value}")
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsIntegerType(Type type)
|
||||
{
|
||||
return type == typeof(sbyte) || type == typeof(short) || type == typeof(int) || type == typeof(long)
|
||||
|| type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong);
|
||||
}
|
||||
|
||||
public static bool IsFloatType(Type type)
|
||||
{
|
||||
return type == typeof(float) || type == typeof(double);
|
||||
}
|
||||
|
||||
public static bool IsNumberType(Type type)
|
||||
{
|
||||
return IsIntegerType(type) || IsFloatType(type);
|
||||
}
|
||||
|
||||
public static T ParseTomlValue<T>(TomlValue value)
|
||||
{
|
||||
return (T)ParseTomlValue(typeof(T), value);
|
||||
}
|
||||
|
||||
public static object ParseTomlValue(Type type, TomlValue value)
|
||||
{
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
return IsTruty(value);
|
||||
}
|
||||
else if (IsNumberType(type))
|
||||
{
|
||||
if (TryGetTomlNumberObject(value, out var numberObject))
|
||||
{
|
||||
return Convert.ChangeType(numberObject, type);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidCastException($"Non-number TOML type: {value.GetType().Name}");
|
||||
}
|
||||
}
|
||||
else if (type == typeof(string))
|
||||
{
|
||||
if (value is TomlString @string)
|
||||
{
|
||||
return @string.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidCastException($"Non-string TOML type: {value.GetType().Name}");
|
||||
}
|
||||
}
|
||||
else if (type.IsEnum)
|
||||
{
|
||||
if (value is TomlString @string)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Enum.Parse(type, @string.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidCastException($"Invalid enum {type.FullName} value: {@string.SerializedValue}");
|
||||
}
|
||||
}
|
||||
else if (value is TomlLong @long)
|
||||
{
|
||||
if (Enum.IsDefined(type, @long.Value))
|
||||
{
|
||||
try
|
||||
{
|
||||
return Enum.ToObject(type, @long.Value);
|
||||
}
|
||||
catch
|
||||
{}
|
||||
}
|
||||
throw new InvalidCastException($"Invalid enum {type.FullName} value: {@long.Value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidCastException($"Non-enum TOML type: {value.GetType().Name}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentMethod = MethodBase.GetCurrentMethod();
|
||||
throw new NotImplementedException($"Unsupported config entry type: {type.FullName}. Please implement in {currentMethod.DeclaringType.FullName}.{currentMethod.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetTomlNumberObject(TomlValue value, out object numberObject)
|
||||
{
|
||||
if (value is TomlLong @long)
|
||||
{
|
||||
numberObject = @long.Value;
|
||||
return true;
|
||||
}
|
||||
else if (value is TomlDouble @double)
|
||||
{
|
||||
numberObject = @double.Value;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
numberObject = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TomlTryGetValueCaseInsensitive(TomlTable table, string key, out TomlValue value)
|
||||
{
|
||||
// Prefer exact match
|
||||
if (table.TryGetValue(key, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Fallback to case-insensitive match
|
||||
foreach (var kvp in table)
|
||||
{
|
||||
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = kvp.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TomlContainsKeyCaseInsensitive(TomlTable table, string key)
|
||||
{
|
||||
// Prefer exact match
|
||||
if (table.ContainsKey(key))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Fallback to case-insensitive match
|
||||
foreach (var kvp in table)
|
||||
{
|
||||
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string ToPascalCase(string str)
|
||||
{
|
||||
return str.Length switch
|
||||
{
|
||||
0 => str,
|
||||
1 => char.ToUpperInvariant(str[0]).ToString(),
|
||||
_ => char.ToUpperInvariant(str[0]) + str.Substring(1)
|
||||
};
|
||||
}
|
||||
|
||||
// We can test the configuration related code without loading the mod into the game.
|
||||
public static void Log(string message)
|
||||
{
|
||||
LogFunction(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user