Files
Shigure/Modules/ModuleStore.cs
waynebian01 232fa91aa2 Add SpellFieldKey class for managing spell and aura keys
- Introduced a new class `SpellFieldKey` to encapsulate constants and methods related to spell and aura identifiers.
- Added methods for generating keys for spells and auras, including parsing functionality for spell and aura strings.
- Implemented utility methods for stripping prefixes and canonicalizing aura IDs.
2026-08-30 10:22:51 +08:00

1836 lines
62 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace Shigure;
public sealed class ModuleDefinition
{
internal const int CurrentUnitMappingVersion = 3;
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = "新模块";
public string Author { get; set; } = string.Empty;
public string RecommendedTalent { get; set; } = string.Empty;
// 保存时写入当时的 Shigure 版本(AppInfo.Version)。
public string Version { get; set; } = string.Empty;
// v2: 31=玩家、32=目标、33=焦点、34=地面、35=鼠标v3: 36/37 从目标迁移为引导/非引导宏条件。
public int? UnitMappingVersion { get; set; }
public bool Enabled { get; set; } = true;
public ModuleMatch Match { get; set; } = new();
public List<ModuleUnit> Units { get; set; } = new();
public List<ModuleCountField> Counts { get; set; } = new();
public List<ModuleValueAdjustment> ValueAdjustments { get; set; } = new();
public List<ModuleRule> Rules { get; set; } = new();
public ModuleDependencySnapshot? Dependencies { get; set; }
[JsonIgnore]
public string? FilePath { get; set; }
public ModuleDefinition Clone()
{
return new ModuleDefinition
{
Id = Id,
Name = Name,
Author = Author,
RecommendedTalent = RecommendedTalent,
Version = Version,
UnitMappingVersion = UnitMappingVersion,
Enabled = Enabled,
FilePath = FilePath,
Match = Match.Clone(),
Units = Units.Select(unit => unit.Clone()).ToList(),
Counts = Counts.Select(count => count.Clone()).ToList(),
ValueAdjustments = ValueAdjustments.Select(adjustment => adjustment.Clone()).ToList(),
Rules = Rules.Select(rule => rule.Clone()).ToList(),
Dependencies = Dependencies?.Clone()
};
}
public static ModuleDefinition CreateDefault(string name = "新模块")
{
return new ModuleDefinition
{
Id = ModuleStore.CreateModuleId(name),
Name = name,
Version = AppInfo.Version,
UnitMappingVersion = CurrentUnitMappingVersion,
Enabled = true,
Rules = []
};
}
}
public sealed class ModuleMatch
{
public int? ClassId { get; set; }
public int? SpecId { get; set; }
public string? PartyType { get; set; }
public int? HeroTalent { get; set; }
[JsonIgnore]
public int Specificity =>
Count(ClassId) + Count(SpecId) + Count(PartyType) + Count(HeroTalent);
public bool Matches(int? classId, int? specId, int? partyType, int? heroTalent)
{
return MatchesOne(ClassId, classId)
&& MatchesOne(SpecId, specId)
&& MatchesPartyType(PartyType, partyType)
&& MatchesOne(HeroTalent, heroTalent);
}
public ModuleMatch Clone()
{
return new ModuleMatch
{
ClassId = ClassId,
SpecId = SpecId,
PartyType = NormalizePartyTypeValue(PartyType),
HeroTalent = HeroTalent
};
}
public static string? NormalizePartyTypeValue(string? value)
{
var text = value?.Trim();
if (string.IsNullOrWhiteSpace(text)
|| text == "*"
|| string.Equals(text, "any", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (string.Equals(text, "单人", StringComparison.OrdinalIgnoreCase))
{
return "0";
}
if (string.Equals(text, "团队", StringComparison.OrdinalIgnoreCase))
{
return "1-40";
}
if (string.Equals(text, "队伍", StringComparison.OrdinalIgnoreCase))
{
return "46";
}
if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var number)
|| int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out number))
{
return number is >= 1 and <= 40 ? "1-40" : number.ToString(CultureInfo.InvariantCulture);
}
var rangeParts = text.Split('-', 2, StringSplitOptions.TrimEntries);
if (rangeParts.Length == 2
&& int.TryParse(rangeParts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var start)
&& int.TryParse(rangeParts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var end))
{
return start <= end
? $"{start.ToString(CultureInfo.InvariantCulture)}-{end.ToString(CultureInfo.InvariantCulture)}"
: $"{end.ToString(CultureInfo.InvariantCulture)}-{start.ToString(CultureInfo.InvariantCulture)}";
}
return text;
}
private static bool MatchesOne(int? expected, int? actual)
{
return expected is null || actual == expected;
}
private static bool MatchesPartyType(string? expected, int? actual)
{
var normalized = NormalizePartyTypeValue(expected);
if (normalized is null)
{
return true;
}
if (actual is null)
{
return false;
}
var rangeParts = normalized.Split('-', 2, StringSplitOptions.TrimEntries);
if (rangeParts.Length == 2
&& int.TryParse(rangeParts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var start)
&& int.TryParse(rangeParts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var end))
{
return actual.Value >= start && actual.Value <= end;
}
return int.TryParse(normalized, NumberStyles.Integer, CultureInfo.InvariantCulture, out var exact)
&& actual.Value == exact;
}
private static int Count(int? value)
{
return value is null ? 0 : 1;
}
private static int Count(string? value)
{
return NormalizePartyTypeValue(value) is null ? 0 : 1;
}
}
public sealed class ModuleRule
{
public bool Enabled { get; set; } = true;
public string Condition { get; set; } = string.Empty;
public string Comment { get; set; } = string.Empty;
// 此规则命中后两次实际发送之间的最小间隔毫秒null/0 表示不限制。
public int? DelayMs { get; set; }
// 此规则实际发送按键后暂停整个逻辑循环的时长毫秒null/0 表示不暂停。
public int? LogicDelayMs { get; set; }
public int? Unit { get; set; }
public string? UnitName { get; set; }
public string Spell { get; set; } = string.Empty;
// null 表示升级前的旧模块(运行时沿用原二元匹配);空字符串表示明确选择无宏条件。
public string? MacroCondition { get; set; }
public string Hotkey { get; set; } = string.Empty;
public string Step { get; set; } = string.Empty;
// 子条件: 与主条件是「且」关系, 子条件之间是「或」关系。
// 命中 = 主条件成立 && (无子条件 || 任一子条件成立)。用于表达求值器写不出的 主 && (A || B)。
// 旧模块无此字段 → 反序列化为 null; 序列化时 null 会被 WhenWritingNull 忽略。
public List<string>? SubConditions { get; set; }
public ModuleRule Clone()
{
return new ModuleRule
{
Enabled = Enabled,
Condition = Condition,
Comment = Comment,
DelayMs = DelayMs,
LogicDelayMs = LogicDelayMs,
Unit = Unit,
UnitName = UnitName,
Spell = Spell,
MacroCondition = MacroCondition,
Hotkey = Hotkey,
Step = Step,
SubConditions = SubConditions is null ? null : new List<string>(SubConditions)
};
}
// 供求值日志与规则表显示复用的可读描述, 避免在 UI 里另写一份。
public string DescribeCondition()
{
if (SubConditions is not { Count: > 0 })
{
return Condition;
}
var any = string.Join(" | ", SubConditions);
return string.IsNullOrWhiteSpace(Condition)
? $"任一({any})"
: $"{Condition} 且任一({any})";
}
}
public sealed class ModuleValueAdjustment
{
public bool Enabled { get; set; } = true;
public string Condition { get; set; } = string.Empty;
public string Field { get; set; } = string.Empty;
public int Delta { get; set; }
public string Formula { get; set; } = string.Empty;
public ModuleValueAdjustment Clone()
{
return new ModuleValueAdjustment
{
Enabled = Enabled,
Condition = Condition,
Field = Field,
Delta = Delta,
Formula = Formula
};
}
}
public sealed class ModuleStore
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters =
{
new JsonStringEnumConverter(),
new StringOrNumberJsonConverter()
}
};
private readonly object _gate = new();
private List<ModuleDefinition> _modules = new();
private readonly HashSet<string> _incompatibleVersionModuleIds = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _rejectedModuleIds = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _importIssueModuleIds = new(StringComparer.OrdinalIgnoreCase);
public ModuleStore(string moduleDirectory)
{
ModuleDirectory = moduleDirectory;
Directory.CreateDirectory(ModuleDirectory);
Reload();
}
public string ModuleDirectory { get; }
public static string ResolveModuleDirectory()
{
return Path.Combine(AppPaths.UserDataDirectory, "module");
}
public IReadOnlyList<ModuleDefinition> GetModules()
{
lock (_gate)
{
return _modules
.Where(module => !_incompatibleVersionModuleIds.Contains(module.Id)
&& !_rejectedModuleIds.Contains(module.Id))
.Select(module => module.Clone())
.ToList();
}
}
public IReadOnlyList<ModuleDefinition> GetModulesForDisplay()
{
lock (_gate)
{
return _modules.Select(module => module.Clone()).ToList();
}
}
public void SetImportIssues(
IEnumerable<string> rejectedModuleIds,
IEnumerable<string> conflictedModuleIds)
{
lock (_gate)
{
_rejectedModuleIds.Clear();
_rejectedModuleIds.UnionWith(rejectedModuleIds);
_importIssueModuleIds.Clear();
_importIssueModuleIds.UnionWith(_rejectedModuleIds);
_importIssueModuleIds.UnionWith(conflictedModuleIds);
}
}
public bool HasImportIssue(string moduleId)
{
lock (_gate)
{
return _incompatibleVersionModuleIds.Contains(moduleId)
|| _importIssueModuleIds.Contains(moduleId);
}
}
public static bool HasCompatibleVersion(ModuleDefinition module)
=> string.Equals(module.Version?.Trim(), AppInfo.Version.Trim(), StringComparison.Ordinal);
public void Reload()
{
lock (_gate)
{
Directory.CreateDirectory(ModuleDirectory);
var loaded = new List<ModuleDefinition>();
_incompatibleVersionModuleIds.Clear();
foreach (var file in Directory.EnumerateFiles(ModuleDirectory, "*.json", SearchOption.AllDirectories))
{
try
{
var module = JsonSerializer.Deserialize<ModuleDefinition>(File.ReadAllText(file), JsonOptions);
if (module is null)
{
continue;
}
Normalize(module);
module.FilePath = file;
loaded.Add(module);
if (!HasCompatibleVersion(module))
{
_incompatibleVersionModuleIds.Add(module.Id);
}
}
catch
{
// 单个模块损坏时跳过,避免影响其它模块加载。
}
}
_modules = SortModules(loaded).ToList();
_rejectedModuleIds.Clear();
_importIssueModuleIds.Clear();
}
}
public ModuleDefinition? FindSelectedOrBestMatch(string? selectedModuleId, int? classId, int? specId, int? partyType, int? heroTalent)
{
lock (_gate)
{
var matches = SortMatches(
_modules.Where(module => !_incompatibleVersionModuleIds.Contains(module.Id)
&& !_rejectedModuleIds.Contains(module.Id)),
classId,
specId,
partyType,
heroTalent)
.ToList();
if (!string.IsNullOrWhiteSpace(selectedModuleId))
{
var selected = matches.FirstOrDefault(module =>
string.Equals(module.Id, selectedModuleId, StringComparison.OrdinalIgnoreCase));
if (selected is not null)
{
return selected.Clone();
}
}
return matches.FirstOrDefault()?.Clone();
}
}
public IReadOnlyList<ModuleDefinition> FindMatches(int? classId, int? specId, int? partyType, int? heroTalent)
{
lock (_gate)
{
return SortMatches(
_modules.Where(module => !_incompatibleVersionModuleIds.Contains(module.Id)
&& !_rejectedModuleIds.Contains(module.Id)),
classId,
specId,
partyType,
heroTalent)
.Select(module => module.Clone())
.ToList();
}
}
public ModuleDefinition Save(ModuleDefinition module)
{
Normalize(module);
var oldPath = module.FilePath;
var path = BuildModulePath(module);
lock (_gate)
{
if (_modules.Any(existing =>
!string.Equals(existing.Id, module.Id, StringComparison.OrdinalIgnoreCase)
&& string.Equals(existing.Name, module.Name, StringComparison.CurrentCultureIgnoreCase)))
{
throw new InvalidOperationException($"模块名称“{module.Name}”已存在。");
}
if (File.Exists(path)
&& (string.IsNullOrWhiteSpace(oldPath) || !PathsEqual(oldPath, path)))
{
throw new InvalidOperationException($"模块文件“{Path.GetFileName(path)}”已存在,请使用其他名称。");
}
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
WriteFileAtomically(path, JsonSerializer.Serialize(module, JsonOptions));
if (!string.IsNullOrWhiteSpace(oldPath)
&& IsInsideModuleDirectory(oldPath)
&& !PathsEqual(oldPath, path)
&& File.Exists(oldPath))
{
try
{
File.Delete(oldPath);
}
catch (Exception deleteError)
{
try
{
File.Delete(path);
}
catch (Exception rollbackError)
{
throw new AggregateException(
"模块已写入新文件,但旧文件删除失败,且无法回滚新文件。",
deleteError,
rollbackError);
}
throw;
}
}
module.FilePath = path;
_modules.RemoveAll(existing =>
string.Equals(existing.Id, module.Id, StringComparison.OrdinalIgnoreCase)
|| string.Equals(existing.FilePath, path, StringComparison.OrdinalIgnoreCase));
_modules.Add(module.Clone());
_modules = SortModules(_modules).ToList();
UpdateVersionCompatibility(module);
_rejectedModuleIds.Remove(module.Id);
_importIssueModuleIds.Remove(module.Id);
return module.Clone();
}
}
/// <summary>
/// 回写已更新的模块依赖快照,并保留模块原有文件位置。
/// </summary>
public ModuleDefinition SaveDependenciesInPlace(ModuleDefinition module)
{
Normalize(module);
lock (_gate)
{
var existing = _modules.FirstOrDefault(item =>
string.Equals(item.Id, module.Id, StringComparison.OrdinalIgnoreCase));
if (existing is null || string.IsNullOrWhiteSpace(existing.FilePath))
{
throw new InvalidOperationException($"找不到要清理的模块“{module.Name}”。");
}
var path = existing.FilePath;
if (!IsInsideModuleDirectory(path) || !File.Exists(path))
{
throw new InvalidOperationException($"模块文件不在模块目录中或已不存在: {path}");
}
module.FilePath = path;
WriteFileAtomically(path, JsonSerializer.Serialize(module, JsonOptions));
_modules.Remove(existing);
_modules.Add(module.Clone());
_modules = SortModules(_modules).ToList();
UpdateVersionCompatibility(module);
_rejectedModuleIds.Remove(module.Id);
_importIssueModuleIds.Remove(module.Id);
return module.Clone();
}
}
public void Delete(ModuleDefinition module)
{
lock (_gate)
{
if (!string.IsNullOrWhiteSpace(module.FilePath)
&& IsInsideModuleDirectory(module.FilePath)
&& File.Exists(module.FilePath))
{
File.Delete(module.FilePath);
}
_modules.RemoveAll(existing =>
string.Equals(existing.Id, module.Id, StringComparison.OrdinalIgnoreCase)
|| string.Equals(existing.FilePath, module.FilePath, StringComparison.OrdinalIgnoreCase));
_incompatibleVersionModuleIds.Remove(module.Id);
_rejectedModuleIds.Remove(module.Id);
_importIssueModuleIds.Remove(module.Id);
}
}
private void UpdateVersionCompatibility(ModuleDefinition module)
{
if (HasCompatibleVersion(module))
{
_incompatibleVersionModuleIds.Remove(module.Id);
}
else
{
_incompatibleVersionModuleIds.Add(module.Id);
}
}
public static string CreateModuleId(string name)
{
return $"{SanitizeFileName(name)}-{DateTimeOffset.Now:yyyyMMddHHmmssfff}";
}
public string CreateNextModuleName()
{
lock (_gate)
{
for (var index = 1; ; index++)
{
var name = $"新模块{index.ToString(CultureInfo.InvariantCulture)}";
if (!_modules.Any(module => string.Equals(module.Name, name, StringComparison.CurrentCultureIgnoreCase)))
{
return name;
}
}
}
}
private static void WriteFileAtomically(string path, string content)
{
var directory = Path.GetDirectoryName(path)!;
var tempPath = Path.Combine(
directory,
$".{Path.GetFileName(path)}.{Guid.NewGuid():N}.tmp");
try
{
File.WriteAllText(tempPath, content);
if (File.Exists(path))
{
File.Move(tempPath, path, overwrite: true);
}
else
{
File.Move(tempPath, path);
}
}
catch
{
try
{
if (File.Exists(tempPath))
{
File.Delete(tempPath);
}
}
catch
{
// 保留原始写入异常;残留临时文件不会被模块扫描加载。
}
throw;
}
}
private static IEnumerable<ModuleDefinition> SortModules(IEnumerable<ModuleDefinition> modules)
{
return modules
.OrderBy(module => module.Match.ClassId ?? int.MaxValue)
.ThenBy(module => module.Match.SpecId ?? int.MaxValue)
.ThenBy(module => PartyTypeSortKey(module.Match.PartyType))
.ThenBy(module => module.Match.HeroTalent ?? int.MaxValue)
.ThenBy(module => module.Name, StringComparer.CurrentCultureIgnoreCase);
}
private static IEnumerable<ModuleDefinition> SortMatches(
IEnumerable<ModuleDefinition> modules,
int? classId,
int? specId,
int? partyType,
int? heroTalent)
{
return modules
.Where(module => module.Match.Matches(classId, specId, partyType, heroTalent))
.OrderByDescending(module => module.Match.Specificity)
.ThenBy(module => module.Name, StringComparer.CurrentCultureIgnoreCase);
}
private string BuildModulePath(ModuleDefinition module)
{
var fileName = $"{SanitizeFileName(module.Name)}.json";
return Path.Combine(ModuleDirectory, fileName);
}
private static bool PathsEqual(string left, string right)
{
return string.Equals(Path.GetFullPath(left), Path.GetFullPath(right), StringComparison.OrdinalIgnoreCase);
}
private static void Normalize(ModuleDefinition module)
{
if (string.IsNullOrWhiteSpace(module.Name))
{
module.Name = "新模块";
}
if (string.IsNullOrWhiteSpace(module.Id))
{
module.Id = CreateModuleId(module.Name);
}
module.Match ??= new ModuleMatch();
module.Match.PartyType = ModuleMatch.NormalizePartyTypeValue(module.Match.PartyType);
module.Units ??= new List<ModuleUnit>();
module.Counts ??= new List<ModuleCountField>();
module.ValueAdjustments ??= new List<ModuleValueAdjustment>();
module.Units.RemoveAll(unit => string.IsNullOrWhiteSpace(unit.Name));
module.Counts.RemoveAll(count => string.IsNullOrWhiteSpace(count.Name));
module.ValueAdjustments.RemoveAll(adjustment => string.IsNullOrWhiteSpace(adjustment.Field));
foreach (var unit in module.Units)
{
unit.Name = unit.Name.Trim();
unit.HealthName = string.IsNullOrWhiteSpace(unit.HealthName) ? null : unit.HealthName.Trim();
unit.HealthThresholdField = string.IsNullOrWhiteSpace(unit.HealthThresholdField) ? null : unit.HealthThresholdField.Trim();
}
foreach (var count in module.Counts)
{
count.Name = count.Name.Trim();
count.HealthThresholdField = string.IsNullOrWhiteSpace(count.HealthThresholdField) ? null : count.HealthThresholdField.Trim();
}
foreach (var adjustment in module.ValueAdjustments)
{
adjustment.Field = adjustment.Field.Trim();
adjustment.Condition = adjustment.Condition?.Trim() ?? string.Empty;
adjustment.Formula = adjustment.Formula?.Trim() ?? string.Empty;
}
module.Rules ??= new List<ModuleRule>();
var unitMappingVersion = module.UnitMappingVersion.GetValueOrDefault();
if (unitMappingVersion < 2)
{
foreach (var rule in module.Rules)
{
rule.Unit = rule.Unit switch
{
31 => ReservedUnit.Cursor,
34 => ReservedUnit.Player,
_ => rule.Unit
};
}
}
if (unitMappingVersion < 3)
{
foreach (var rule in module.Rules)
{
if (rule.Unit is 36 or 37)
{
var normalizedMacro = MacroConditionText.NormalizeLegacyUnit(rule.Unit.Value, rule.MacroCondition);
rule.Unit = normalizedMacro.Unit;
rule.MacroCondition = normalizedMacro.Condition;
}
}
}
module.UnitMappingVersion = ModuleDefinition.CurrentUnitMappingVersion;
foreach (var rule in module.Rules)
{
rule.Comment = rule.Comment?.Trim() ?? string.Empty;
rule.Spell = ModuleSpecialActions.NormalizeSpellAction(rule.Spell);
if (rule.MacroCondition is not null)
{
rule.MacroCondition = MacroConditionText.Normalize(rule.MacroCondition);
}
rule.DelayMs = rule.DelayMs is > 0 ? rule.DelayMs : null;
rule.LogicDelayMs = rule.LogicDelayMs is > 0 ? rule.LogicDelayMs : null;
if (rule.SubConditions is null)
{
continue;
}
// 去空白、丢空项; 整组为空则回到 null, 保持文件干净且求值不见空子条件。
rule.SubConditions = rule.SubConditions
.Select(sub => sub?.Trim() ?? string.Empty)
.Where(sub => sub.Length > 0)
.ToList();
if (rule.SubConditions.Count == 0)
{
rule.SubConditions = null;
}
}
}
private bool IsInsideModuleDirectory(string path)
{
var fullDirectory = Path.GetFullPath(ModuleDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var fullPath = Path.GetFullPath(path);
return fullPath.StartsWith(fullDirectory + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}
private static string SanitizeFileName(string value)
{
var text = string.IsNullOrWhiteSpace(value) ? "module" : value.Trim();
foreach (var invalid in Path.GetInvalidFileNameChars())
{
text = text.Replace(invalid, '-');
}
text = Regex.Replace(text, @"\s+", "-");
return text.Length > 64 ? text[..64] : text;
}
private static int PartyTypeSortKey(string? value)
{
return ModuleMatch.NormalizePartyTypeValue(value) switch
{
null => int.MaxValue,
"0" => 0,
"1-40" => 1,
"46" => 46,
var other when int.TryParse(other, NumberStyles.Integer, CultureInfo.InvariantCulture, out var number) => number,
_ => int.MaxValue - 1
};
}
private sealed class StringOrNumberJsonConverter : JsonConverter<string?>
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number when reader.TryGetInt64(out var integer) => integer.ToString(CultureInfo.InvariantCulture),
JsonTokenType.Number when reader.TryGetDouble(out var number) => number.ToString(CultureInfo.InvariantCulture),
JsonTokenType.True => "true",
JsonTokenType.False => "false",
_ => throw new JsonException($"无法将 {reader.TokenType} 转换为字符串。")
};
}
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}
writer.WriteStringValue(value);
}
}
}
public static class ModuleLogic
{
public static LogicDecision Run(ModuleDefinition module, GameState state, IKeymapResolver keymap)
{
var info = CreateInfo(module, state);
var spellIndices = keymap.GetCurrentSpellIndices();
var unitSlots = ResolveDynamicFields(module, state, spellIndices);
var failedSpells = keymap.GetCurrentFailedSpells();
var oneKeySpells = keymap.GetCurrentOneKeySpells();
var spellNames = keymap.GetCurrentSpellNames();
for (var ruleIndex = 0; ruleIndex < module.Rules.Count; ruleIndex++)
{
var rule = module.Rules[ruleIndex];
var rateLimitKey = $"{module.Id}:{ruleIndex}";
if (!rule.Enabled)
{
continue;
}
if (!ModuleConditionEvaluator.TryEvaluateRule(
rule,
state,
out var conditionMatched,
out var error,
failedSpells,
spellIndices))
{
info["条件错误"] = error;
info["规则条件"] = rule.DescribeCondition();
AddRuleLogInfo(info, rule, ruleIndex, rateLimitKey, null);
return new LogicDecision(null, $"{module.Name}: 条件错误", info, module.Name);
}
if (!conditionMatched)
{
continue;
}
if (ModuleSpecialActions.IsPauseSpell(rule.Spell))
{
info["命中条件"] = string.IsNullOrWhiteSpace(rule.DescribeCondition()) ? "始终" : rule.DescribeCondition();
info["动作技能"] = ModuleSpecialActions.PauseSpell;
info["动作按键"] = "-";
info["动作单位"] = "-";
AddRuleLogInfo(info, rule, ruleIndex, rateLimitKey, null);
return new LogicDecision(null, $"{module.Name}: 暂停", info, module.Name);
}
var resolvedUnit = rule.Unit;
if (!string.IsNullOrWhiteSpace(rule.UnitName))
{
// 动态目标: 选择器没选中任何单位时跳过该规则(等同条件未命中)。
var slot = unitSlots.TryGetValue(rule.UnitName, out var s) ? s : null;
if (slot is null)
{
continue;
}
resolvedUnit = int.TryParse(slot, out var slotUnit) ? slotUnit : 0;
}
long? actionSpellId = null;
var actionSpellName = rule.Spell.Trim();
var isOneKeySpell = false;
if (ModuleSpecialActions.IsFailedSpell(rule.Spell))
{
actionSpellId = ModuleSpecialActions.GetFailedSpell(state, failedSpells);
if (actionSpellId is null)
{
continue;
}
if (!spellNames.TryGetValue(actionSpellId.Value, out actionSpellName))
{
continue;
}
}
else if (ModuleSpecialActions.IsOneKeySpell(rule.Spell))
{
isOneKeySpell = true;
actionSpellId = ModuleSpecialActions.GetOneKeySpell(state, oneKeySpells);
if (actionSpellId is null)
{
continue;
}
if (!spellNames.TryGetValue(actionSpellId.Value, out actionSpellName))
{
continue;
}
resolvedUnit = 0;
}
else if (long.TryParse(actionSpellName, NumberStyles.Integer, CultureInfo.InvariantCulture, out var legacySpellId)
&& legacySpellId > 0
&& spellNames.TryGetValue(legacySpellId, out var legacySpellName))
{
// 兼容曾以 spellId 保存的普通动作;编辑器再次保存时会落回技能名称。
actionSpellName = legacySpellName;
}
var resolvedMacroCondition = rule.MacroCondition;
var hotkey = string.IsNullOrWhiteSpace(rule.Hotkey)
? actionSpellId is { } id
? keymap.GetHotkey(resolvedUnit, id, resolvedMacroCondition)
: keymap.GetHotkey(resolvedUnit, actionSpellName, resolvedMacroCondition)
: rule.Hotkey.Trim();
if (isOneKeySpell
&& string.IsNullOrWhiteSpace(rule.Hotkey)
&& string.IsNullOrWhiteSpace(hotkey)
&& actionSpellId is not null)
{
hotkey = keymap.GetHotkey(ReservedUnit.None, actionSpellId.Value, MacroConditionText.NoChanneling);
if (!string.IsNullOrWhiteSpace(hotkey))
{
resolvedUnit = ReservedUnit.None;
resolvedMacroCondition = MacroConditionText.NoChanneling;
}
}
var step = BuildStep(module, rule, hotkey, actionSpellName);
info["命中条件"] = string.IsNullOrWhiteSpace(rule.Condition) ? "始终" : rule.Condition;
info["动作技能"] = string.IsNullOrWhiteSpace(actionSpellName)
? "-"
: actionSpellId is null ? actionSpellName : $"{actionSpellName} / {actionSpellId}";
info["宏条件"] = string.IsNullOrWhiteSpace(resolvedMacroCondition)
? "-"
: MacroConditionText.ToDisplayText(resolvedMacroCondition);
info["动作按键"] = string.IsNullOrWhiteSpace(hotkey) ? "-" : hotkey;
info["动作单位"] = string.IsNullOrWhiteSpace(rule.UnitName)
? resolvedUnit.GetValueOrDefault()
: $"{rule.UnitName} → {resolvedUnit.GetValueOrDefault()}";
AddRuleLogInfo(info, rule, ruleIndex, rateLimitKey, hotkey);
return new LogicDecision(
hotkey,
step,
info,
module.Name,
rule.DelayMs.GetValueOrDefault(),
rateLimitKey,
rule.LogicDelayMs.GetValueOrDefault());
}
info["命中条件"] = "-";
return new LogicDecision(null, $"{module.Name}: 无匹配规则", info, module.Name);
}
private static void AddRuleLogInfo(
IDictionary<string, object?> info,
ModuleRule rule,
int ruleIndex,
string rateLimitKey,
string? hotkey)
{
info["动作按键"] = string.IsNullOrWhiteSpace(hotkey) ? "-" : hotkey;
info["动作延迟"] = rule.DelayMs is > 0 ? $"{rule.DelayMs.Value} ms" : "-";
info["逻辑延迟"] = rule.LogicDelayMs is > 0 ? $"{rule.LogicDelayMs.Value} ms" : "-";
info["规则编号"] = ruleIndex + 1;
info["限流键"] = rateLimitKey;
}
// 把模块定义的动态单位/数量各解析一次, 写入当前帧 state.Values 供条件求值与目标解析使用。
public static Dictionary<string, string?> ResolveDynamicFields(
ModuleDefinition module,
GameState state,
IReadOnlyDictionary<long, int>? spellIndices = null)
{
if (IsDynamicFieldsResolved(module, state)
&& state.Values.TryGetValue("$units", out var existingUnitsObj)
&& existingUnitsObj is Dictionary<string, string?> existingUnits)
{
return existingUnits;
}
var earlyAppliedAdjustments = ApplyValueAdjustments(
module,
state,
spellIndices,
adjustment => IsEarlyThresholdAdjustment(module, state, adjustment));
var unitSlots = ResolveUnits(module, state);
ResolveCounts(module, state);
ApplyValueAdjustments(
module,
state,
spellIndices,
adjustment => !earlyAppliedAdjustments.Contains(adjustment));
state.Values["$dynamicModuleId"] = module.Id;
return unitSlots;
}
private static bool IsDynamicFieldsResolved(ModuleDefinition module, GameState state)
{
return state.Values.TryGetValue("$dynamicModuleId", out var value)
&& string.Equals(value?.ToString(), module.Id, StringComparison.OrdinalIgnoreCase);
}
private static Dictionary<string, string?> ResolveUnits(ModuleDefinition module, GameState state)
{
var unitSlots = new Dictionary<string, string?>(StringComparer.Ordinal);
// 生命值名 → 该单位槽位的 生命值 值(未解析则为 null), 供条件直接按名引用。
var unitHealth = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (var unit in module.Units)
{
if (string.IsNullOrWhiteSpace(unit.Name))
{
continue;
}
var slot = UnitSelector.Resolve(unit, state);
unitSlots[unit.Name] = slot;
if (!string.IsNullOrWhiteSpace(unit.HealthName))
{
unitHealth[unit.HealthName] = slot is not null
&& state.Group.TryGetValue(slot, out var member)
&& member.TryGetValue("生命值", out var value)
? value
: null;
}
}
state.Values["$units"] = unitSlots;
state.Values["$unithealth"] = unitHealth;
return unitSlots;
}
private static void ResolveCounts(ModuleDefinition module, GameState state)
{
var counts = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var count in module.Counts)
{
if (!string.IsNullOrWhiteSpace(count.Name))
{
counts[count.Name] = UnitSelector.Resolve(count, state);
}
}
state.Values["$counts"] = counts;
}
private static HashSet<ModuleValueAdjustment> ApplyValueAdjustments(
ModuleDefinition module,
GameState state,
IReadOnlyDictionary<long, int>? spellIndices,
Func<ModuleValueAdjustment, bool>? include = null)
{
var applied = new HashSet<ModuleValueAdjustment>();
foreach (var adjustment in module.ValueAdjustments.Where(adjustment => adjustment.Enabled))
{
if (string.IsNullOrWhiteSpace(adjustment.Field)
|| (adjustment.Delta == 0 && string.IsNullOrWhiteSpace(adjustment.Formula)))
{
continue;
}
if (include is not null && !include(adjustment))
{
continue;
}
if (!ModuleConditionEvaluator.TryEvaluate(
adjustment.Condition,
state,
out var matched,
out _,
spellIndices: spellIndices)
|| !matched)
{
continue;
}
if (!ApplyValueAdjustment(state, adjustment))
{
continue;
}
applied.Add(adjustment);
}
return applied;
}
private static bool IsEarlyThresholdAdjustment(
ModuleDefinition module,
GameState state,
ModuleValueAdjustment adjustment)
{
var key = adjustment.Field.Trim();
return key.Length > 0
&& !key.Contains('.')
&& !key.StartsWith('$')
&& DynamicThresholdFields(module).Contains(key);
}
private static HashSet<string> DynamicThresholdFields(ModuleDefinition module)
{
var fields = new HashSet<string>(StringComparer.Ordinal);
foreach (var unit in module.Units)
{
if (!string.IsNullOrWhiteSpace(unit.HealthThresholdField))
{
fields.Add(unit.HealthThresholdField.Trim());
}
}
foreach (var count in module.Counts)
{
if (!string.IsNullOrWhiteSpace(count.HealthThresholdField))
{
fields.Add(count.HealthThresholdField.Trim());
}
}
return fields;
}
private static bool ApplyValueAdjustment(GameState state, ModuleValueAdjustment adjustment)
{
if (!string.IsNullOrWhiteSpace(adjustment.Formula))
{
if (!FormulaEvaluator.TryEvaluateInt(adjustment.Formula, state, out var value, out _))
{
return false;
}
SetDynamicValue(state, adjustment.Field, value);
return true;
}
ApplyValueDelta(state, adjustment.Field, adjustment.Delta);
return true;
}
private static void SetDynamicValue(GameState state, string field, object? value)
{
var key = field.Trim();
if (key.Length == 0)
{
return;
}
if (key.StartsWith("auras.", StringComparison.OrdinalIgnoreCase))
{
GetOrCreateMutableDict(state, "auras")[key["auras.".Length..]] = value;
return;
}
if (key.StartsWith("spells.", StringComparison.OrdinalIgnoreCase))
{
GetOrCreateMutableDict(state, "spells")[key["spells.".Length..]] = value;
return;
}
GetOrCreateDynamicValues(state)[key] = value;
}
private static Dictionary<string, object?> GetOrCreateDynamicValues(GameState state)
{
if (state.Values.TryGetValue("$dynamicvalues", out var dynamicObj))
{
if (dynamicObj is Dictionary<string, object?> dynamicValues)
{
return dynamicValues;
}
if (dynamicObj is IReadOnlyDictionary<string, object?> existingValues)
{
var copiedValues = new Dictionary<string, object?>(existingValues, StringComparer.Ordinal);
state.Values["$dynamicvalues"] = copiedValues;
return copiedValues;
}
}
var values = new Dictionary<string, object?>(StringComparer.Ordinal);
state.Values["$dynamicvalues"] = values;
return values;
}
private static Dictionary<string, object?> GetOrCreateMutableDict(GameState state, string dictKey)
{
if (state.Values.TryGetValue(dictKey, out var obj))
{
if (obj is Dictionary<string, object?> mutable)
{
return mutable;
}
if (obj is IReadOnlyDictionary<string, object?> readOnly)
{
var copy = new Dictionary<string, object?>(readOnly, StringComparer.Ordinal);
state.Values[dictKey] = copy;
return copy;
}
}
var dict = new Dictionary<string, object?>(StringComparer.Ordinal);
state.Values[dictKey] = dict;
return dict;
}
private static void ApplyValueDelta(GameState state, string field, int delta)
{
var key = field.Trim();
if (key.Length == 0)
{
return;
}
if (key.StartsWith("auras.", StringComparison.OrdinalIgnoreCase))
{
var auraKey = key["auras.".Length..];
var dict = GetOrCreateMutableDict(state, "auras");
var current = dict.TryGetValue(auraKey, out var v) ? v : null;
dict[auraKey] = AddDelta(current, delta);
return;
}
if (key.StartsWith("spells.", StringComparison.OrdinalIgnoreCase))
{
var spellKey = key["spells.".Length..];
var dict = GetOrCreateMutableDict(state, "spells");
var current = dict.TryGetValue(spellKey, out var v) ? v : null;
dict[spellKey] = AddDelta(current, delta);
return;
}
if (state.Values.TryGetValue("$counts", out var countsObj)
&& countsObj is Dictionary<string, int> counts
&& counts.TryGetValue(key, out var countValue))
{
counts[key] = countValue + delta;
return;
}
if (state.Values.TryGetValue("$unithealth", out var healthObj)
&& healthObj is Dictionary<string, object?> unitHealth
&& unitHealth.ContainsKey(key))
{
unitHealth[key] = AddDelta(unitHealth[key], delta);
return;
}
state.Values[key] = AddDelta(state.Values.TryGetValue(key, out var value) ? value : null, delta);
}
private static int AddDelta(object? value, int delta)
{
return TryToInt(value, out var number) ? number + delta : delta;
}
private static Dictionary<string, object?> CreateInfo(ModuleDefinition module, GameState state)
{
return new Dictionary<string, object?>
{
["模块"] = module.Name,
["职业"] = module.Match.ClassId?.ToString() ?? "*",
["专精"] = module.Match.SpecId?.ToString() ?? "*",
["队伍类型"] = state.GetInt("队伍类型"),
["英雄天赋"] = state.GetInt("英雄天赋"),
["规则数"] = module.Rules.Count
};
}
private static bool TryToInt(object? value, out int number)
{
switch (value)
{
case int i:
number = i;
return true;
case long l:
number = (int)l;
return true;
case double d:
number = (int)d;
return true;
case decimal m:
number = (int)m;
return true;
case bool b:
number = b ? 1 : 0;
return true;
case string s when int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|| int.TryParse(s, NumberStyles.Integer, CultureInfo.CurrentCulture, out parsed):
number = parsed;
return true;
default:
number = 0;
return false;
}
}
private static string BuildStep(ModuleDefinition module, ModuleRule rule, string? hotkey, string? actionSpell)
{
if (!string.IsNullOrWhiteSpace(rule.Step))
{
return $"{module.Name}: {rule.Step.Trim()}";
}
if (!string.IsNullOrWhiteSpace(rule.Spell))
{
var spell = string.IsNullOrWhiteSpace(actionSpell) ? rule.Spell.Trim() : actionSpell.Trim();
return string.IsNullOrWhiteSpace(hotkey)
? $"{module.Name}: 未找到按键 {spell}"
: $"{module.Name}: 施放 {spell}";
}
return string.IsNullOrWhiteSpace(hotkey)
? $"{module.Name}: 命中规则"
: $"{module.Name}: 发送 {hotkey}";
}
}
public static class ModuleConditionEvaluator
{
private static readonly Regex InRegex = new(
@"^\s*(?<field>.+?)\s+(?<op>not\s+in|in)\s*\((?<value>.*?)\)\s*$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ComparisonRegex = new(
@"^\s*(?<field>.+?)\s*(?<op>==|!=|>=|<=|>|<)\s*(?<value>.+?)\s*$",
RegexOptions.Compiled);
public static bool TryEvaluate(
string? expression,
GameState state,
out bool matched,
out string? error,
IReadOnlyDictionary<int, long>? failedSpells = null,
IReadOnlyDictionary<long, int>? spellIndices = null)
{
matched = false;
error = null;
if (string.IsNullOrWhiteSpace(expression))
{
matched = true;
return true;
}
foreach (var orPart in Regex.Split(expression, @"\s*\|\|\s*"))
{
var allAndMatched = true;
foreach (var andPart in Regex.Split(orPart, @"\s*&&\s*"))
{
if (!TryEvaluateTerm(
andPart,
state,
out var termMatched,
out error,
failedSpells,
spellIndices))
{
return false;
}
if (!termMatched)
{
allAndMatched = false;
break;
}
}
if (allAndMatched)
{
matched = true;
return true;
}
}
matched = false;
return true;
}
// 整条规则的命中判定: 主条件成立 且 (无子条件 || 任一子条件成立)。
// 子条件之间是「或」, 与主条件是「且」; 任一子条件求值出错都按错误返回。
public static bool TryEvaluateRule(
ModuleRule rule,
GameState state,
out bool matched,
out string? error,
IReadOnlyDictionary<int, long>? failedSpells = null,
IReadOnlyDictionary<long, int>? spellIndices = null)
{
if (!TryEvaluate(rule.Condition, state, out matched, out error, failedSpells, spellIndices))
{
return false;
}
if (!matched || rule.SubConditions is not { Count: > 0 })
{
return true;
}
foreach (var sub in rule.SubConditions)
{
if (string.IsNullOrWhiteSpace(sub))
{
continue;
}
if (!TryEvaluate(sub, state, out var subMatched, out error, failedSpells, spellIndices))
{
matched = false;
return false;
}
if (subMatched)
{
matched = true;
return true;
}
}
matched = false;
return true;
}
public static bool TryResolveInt(GameState state, string fieldName, out int value)
{
if (TryResolveDouble(state, fieldName, out var number))
{
value = (int)number;
return true;
}
value = 0;
return false;
}
public static bool TryResolveDouble(GameState state, string fieldName, out double value)
=> TryToDouble(ResolveValue(state, fieldName), out value);
private static bool TryEvaluateTerm(
string term,
GameState state,
out bool matched,
out string? error,
IReadOnlyDictionary<int, long>? failedSpells,
IReadOnlyDictionary<long, int>? spellIndices)
{
matched = false;
error = null;
var trimmed = term.Trim();
if (trimmed.Length == 0)
{
matched = true;
return true;
}
var inMatch = InRegex.Match(trimmed);
if (inMatch.Success)
{
var inField = inMatch.Groups["field"].Value.Trim();
if (SpellIdConditionFields.Contains(inField))
{
error = $"{inField} 仅支持 == 或 != 判断。";
return false;
}
var inLeft = ResolveValue(state, inField, failedSpells);
if (inLeft is null && IsStructuredSpellReference(inField))
{
matched = false;
return true;
}
var inOp = NormalizeOperator(inMatch.Groups["op"].Value);
var values = ParseListLiterals(inMatch.Groups["value"].Value);
return TryCompareIn(inLeft, inOp, values, out matched, out error);
}
var comparison = ComparisonRegex.Match(trimmed);
if (!comparison.Success)
{
var invert = trimmed.StartsWith('!');
var fieldName = invert ? trimmed[1..].Trim() : trimmed;
var value = ResolveValue(state, fieldName, failedSpells);
if (value is null && IsStructuredSpellReference(fieldName))
{
matched = false;
return true;
}
matched = invert ? !IsTruthy(value) : IsTruthy(value);
return true;
}
var comparisonField = comparison.Groups["field"].Value.Trim();
var left = ResolveValue(state, comparisonField, failedSpells);
if (left is null && IsStructuredSpellReference(comparisonField))
{
matched = false;
return true;
}
var op = comparison.Groups["op"].Value;
var right = ParseLiteral(comparison.Groups["value"].Value.Trim());
if (SpellIdConditionFields.Contains(comparisonField))
{
if (op is not ("==" or "!="))
{
error = $"{comparisonField} 仅支持 == 或 != 判断。";
return false;
}
if (!TryToInt64(right, out var spellId)
|| spellIndices is null
|| !spellIndices.TryGetValue(spellId, out var localIndex))
{
matched = false;
return true;
}
right = localIndex;
}
return TryCompare(left, op, right, out matched, out error);
}
private static bool IsStructuredSpellReference(string fieldName)
{
if (SpellFieldKey.TryParseSpell(fieldName, out _, out _)
|| SpellFieldKey.TryParseAura(fieldName, out _, out _, out _))
{
return true;
}
var key = SpellFieldKey.StripRoot(fieldName);
return Regex.IsMatch(key, @"(?:^|\.)auras\.\d+\.(?:value|apps)$", RegexOptions.IgnoreCase);
}
private static bool TryToInt64(object? value, out long number)
{
switch (value)
{
case long longValue:
number = longValue;
return true;
case int intValue:
number = intValue;
return true;
case double doubleValue when doubleValue >= long.MinValue
&& doubleValue <= long.MaxValue
&& Math.Abs(doubleValue % 1) < double.Epsilon:
number = (long)doubleValue;
return true;
case string text when long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed):
number = parsed;
return true;
default:
number = 0;
return false;
}
}
private static object? ResolveValue(
GameState state,
string fieldName,
IReadOnlyDictionary<int, long>? failedSpells = null)
{
var key = fieldName.Trim();
if (key.StartsWith("state.", StringComparison.OrdinalIgnoreCase))
{
key = key["state.".Length..];
}
if (key.StartsWith("spells.", StringComparison.OrdinalIgnoreCase))
{
return state.Spells.TryGetValue(key["spells.".Length..], out var value) ? value : null;
}
if (key.StartsWith("spell.", StringComparison.OrdinalIgnoreCase))
{
return state.Spells.TryGetValue(key["spell.".Length..], out var value) ? value : null;
}
if (key.StartsWith("auras.", StringComparison.OrdinalIgnoreCase))
{
return state.Auras.TryGetValue(key["auras.".Length..], out var value) ? value : null;
}
if (key.StartsWith("aura.", StringComparison.OrdinalIgnoreCase))
{
return state.Auras.TryGetValue(key["aura.".Length..], out var value) ? value : null;
}
if (ModuleSpecialActions.IsFailedSpell(key))
{
return ModuleSpecialActions.GetFailedSpell(state, failedSpells);
}
if (key.StartsWith("group.", StringComparison.OrdinalIgnoreCase))
{
var parts = key.Split('.', 3);
if (parts.Length == 3
&& state.Group.TryGetValue(parts[1], out var unit)
&& unit.TryGetValue(parts[2], out var value))
{
return value;
}
return null;
}
// 数量字段(整名匹配): 如 低血量人数。
if (state.Values.TryGetValue("$counts", out var countsObj)
&& countsObj is Dictionary<string, int> counts
&& counts.TryGetValue(key, out var countValue))
{
return countValue;
}
// 生命值名(整名匹配): 动态单位的 生命值 直接命名, 如 最低血量 < 50。未解析返回 null。
if (state.Values.TryGetValue("$unithealth", out var healthObj)
&& healthObj is Dictionary<string, object?> unitHealth
&& unitHealth.TryGetValue(key, out var healthValue))
{
return healthValue;
}
// 动态单位字段引用: <单位名>.<字段>, 解析槽位后读 group[槽位][字段]; 单位未解析返回 null。
var dot = key.IndexOf('.');
if (dot > 0
&& state.Values.TryGetValue("$units", out var unitsObj)
&& unitsObj is Dictionary<string, string?> units)
{
var unitName = key[..dot];
if (units.TryGetValue(unitName, out var slot))
{
if (slot is null)
{
return null;
}
var field = key[(dot + 1)..];
return state.Group.TryGetValue(slot, out var member) && member.TryGetValue(field, out var value)
? value
: null;
}
}
// 裸单位名作为存在性布尔: 解析到槽位即 true。
if (state.Values.TryGetValue("$units", out var unitsObj2)
&& unitsObj2 is Dictionary<string, string?> units2
&& units2.TryGetValue(key, out var bareSlot))
{
return bareSlot is not null;
}
if (state.Values.TryGetValue("$dynamicvalues", out var dynamicObj)
&& dynamicObj is IReadOnlyDictionary<string, object?> dynamicValues
&& dynamicValues.TryGetValue(key, out var dynamicValue))
{
return dynamicValue;
}
return state.GetValue(key);
}
private static object? ParseLiteral(string value)
{
var text = value.Trim();
if ((text.StartsWith('"') && text.EndsWith('"')) || (text.StartsWith('\'') && text.EndsWith('\'')))
{
return text[1..^1];
}
return text.ToLowerInvariant() switch
{
"null" or "nil" or "空" => null,
"true" or "yes" or "是" => true,
"false" or "no" or "否" => false,
_ => TryParseNumber(text, out var number) ? number : text
};
}
private static bool TryCompare(object? left, string op, object? right, out bool matched, out string? error)
{
matched = false;
error = null;
if (TryToDouble(left, out var leftNumber) && TryToDouble(right, out var rightNumber))
{
matched = op switch
{
"==" => leftNumber == rightNumber,
"!=" => leftNumber != rightNumber,
">" => leftNumber > rightNumber,
">=" => leftNumber >= rightNumber,
"<" => leftNumber < rightNumber,
"<=" => leftNumber <= rightNumber,
_ => false
};
return true;
}
if (op is "==" or "!=")
{
var equals = string.Equals(FormatComparable(left), FormatComparable(right), StringComparison.OrdinalIgnoreCase);
matched = op == "==" ? equals : !equals;
return true;
}
// 关系比较(> < >= <=)遇到非数字/缺失值(如未解析的动态单位字段)时不报错, 视为不命中, 继续判断下一条规则。
matched = false;
return true;
}
private static bool TryCompareIn(object? left, string op, IReadOnlyList<object?> values, out bool matched, out string? error)
{
matched = false;
error = null;
foreach (var value in values)
{
if (!TryCompare(left, "==", value, out var equals, out error))
{
return false;
}
if (equals)
{
matched = op == "in";
return true;
}
}
matched = op == "not in";
return true;
}
private static IReadOnlyList<object?> ParseListLiterals(string value)
{
var values = new List<object?>();
foreach (var item in SplitList(value))
{
var trimmed = item.Trim();
if (trimmed.Length > 0)
{
values.Add(ParseLiteral(trimmed));
}
}
return values;
}
private static IEnumerable<string> SplitList(string value)
{
var start = 0;
var quote = '\0';
for (var i = 0; i < value.Length; i++)
{
var ch = value[i];
if (quote != '\0')
{
if (ch == quote)
{
quote = '\0';
}
continue;
}
if (ch is '"' or '\'')
{
quote = ch;
continue;
}
if (ch == ',')
{
yield return value[start..i];
start = i + 1;
}
}
yield return value[start..];
}
private static string NormalizeOperator(string op)
{
return Regex.Replace(op.Trim().ToLowerInvariant(), @"\s+", " ");
}
private static bool IsTruthy(object? value)
{
return value switch
{
null => false,
bool b => b,
int i => i != 0,
long l => l != 0,
double d => Math.Abs(d) > double.Epsilon,
string s => !string.IsNullOrWhiteSpace(s)
&& !string.Equals(s, "0", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(s, "false", StringComparison.OrdinalIgnoreCase),
_ => true
};
}
private static bool TryParseNumber(string text, out double value)
{
return double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value)
|| double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out value);
}
private static bool TryToDouble(object? value, out double number)
{
switch (value)
{
case int i:
number = i;
return true;
case long l:
number = l;
return true;
case double d:
number = d;
return true;
case bool b:
number = b ? 1 : 0;
return true;
case string s:
return TryParseNumber(s, out number);
default:
number = 0;
return false;
}
}
private static string FormatComparable(object? value)
{
return value switch
{
null => string.Empty,
bool b => b ? "true" : "false",
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString() ?? string.Empty
};
}
}